mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 05:53:10 +08:00
Merge branch 'master' into Liswiera-FL-changes
This commit is contained in:
commit
8c3fbb6eb4
@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
|||||||
if (!effectPoint.KiaiMode)
|
if (!effectPoint.KiaiMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (beatIndex % (int)timingPoint.TimeSignature != 0)
|
if (beatIndex % timingPoint.TimeSignature.Numerator != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
double duration = timingPoint.BeatLength * 2;
|
double duration = timingPoint.BeatLength * 2;
|
||||||
|
@ -178,17 +178,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
var timingPoint = controlPoints.TimingPointAt(0);
|
var timingPoint = controlPoints.TimingPointAt(0);
|
||||||
Assert.AreEqual(956, timingPoint.Time);
|
Assert.AreEqual(956, timingPoint.Time);
|
||||||
Assert.AreEqual(329.67032967033, timingPoint.BeatLength);
|
Assert.AreEqual(329.67032967033, timingPoint.BeatLength);
|
||||||
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
|
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||||
|
|
||||||
timingPoint = controlPoints.TimingPointAt(48428);
|
timingPoint = controlPoints.TimingPointAt(48428);
|
||||||
Assert.AreEqual(956, timingPoint.Time);
|
Assert.AreEqual(956, timingPoint.Time);
|
||||||
Assert.AreEqual(329.67032967033d, timingPoint.BeatLength);
|
Assert.AreEqual(329.67032967033d, timingPoint.BeatLength);
|
||||||
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
|
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||||
|
|
||||||
timingPoint = controlPoints.TimingPointAt(119637);
|
timingPoint = controlPoints.TimingPointAt(119637);
|
||||||
Assert.AreEqual(119637, timingPoint.Time);
|
Assert.AreEqual(119637, timingPoint.Time);
|
||||||
Assert.AreEqual(659.340659340659, timingPoint.BeatLength);
|
Assert.AreEqual(659.340659340659, timingPoint.BeatLength);
|
||||||
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
|
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||||
|
|
||||||
var difficultyPoint = controlPoints.DifficultyPointAt(0);
|
var difficultyPoint = controlPoints.DifficultyPointAt(0);
|
||||||
Assert.AreEqual(0, difficultyPoint.Time);
|
Assert.AreEqual(0, difficultyPoint.Time);
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
bool callbackRan = false;
|
bool callbackRan = false;
|
||||||
|
|
||||||
realmFactory.Run(realm =>
|
realmFactory.RegisterCustomSubscription(realm =>
|
||||||
{
|
{
|
||||||
var subscription = realm.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
|
var subscription = realm.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
|
||||||
{
|
{
|
||||||
@ -60,6 +60,7 @@ namespace osu.Game.Tests.Database
|
|||||||
realmFactory.Run(r => r.Refresh());
|
realmFactory.Run(r => r.Refresh());
|
||||||
|
|
||||||
subscription?.Dispose();
|
subscription?.Dispose();
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.IsTrue(callbackRan);
|
Assert.IsTrue(callbackRan);
|
||||||
|
@ -239,7 +239,7 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
int changesTriggered = 0;
|
int changesTriggered = 0;
|
||||||
|
|
||||||
realmFactory.Run(outerRealm =>
|
realmFactory.RegisterCustomSubscription(outerRealm =>
|
||||||
{
|
{
|
||||||
outerRealm.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
|
outerRealm.All<BeatmapInfo>().QueryAsyncWithNotifications(gotChange);
|
||||||
ILive<BeatmapInfo>? liveBeatmap = null;
|
ILive<BeatmapInfo>? liveBeatmap = null;
|
||||||
@ -282,6 +282,8 @@ namespace osu.Game.Tests.Database
|
|||||||
r.Remove(resolved);
|
r.Remove(resolved);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
|
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
|
||||||
|
138
osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
Normal file
138
osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class RealmSubscriptionRegistrationTests : RealmTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestSubscriptionWithContextLoss()
|
||||||
|
{
|
||||||
|
IEnumerable<BeatmapSetInfo>? resolvedItems = null;
|
||||||
|
ChangeSet? lastChanges = null;
|
||||||
|
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||||
|
|
||||||
|
var registration = realmFactory.RegisterForNotifications(realm => realm.All<BeatmapSetInfo>(), onChanged);
|
||||||
|
|
||||||
|
testEventsArriving(true);
|
||||||
|
|
||||||
|
// All normal until here.
|
||||||
|
// Now let's yank the main realm context.
|
||||||
|
resolvedItems = null;
|
||||||
|
lastChanges = null;
|
||||||
|
|
||||||
|
using (realmFactory.BlockAllOperations())
|
||||||
|
Assert.That(resolvedItems, Is.Empty);
|
||||||
|
|
||||||
|
realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||||
|
|
||||||
|
testEventsArriving(true);
|
||||||
|
|
||||||
|
// Now let's try unsubscribing.
|
||||||
|
resolvedItems = null;
|
||||||
|
lastChanges = null;
|
||||||
|
|
||||||
|
registration.Dispose();
|
||||||
|
|
||||||
|
realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||||
|
|
||||||
|
testEventsArriving(false);
|
||||||
|
|
||||||
|
// And make sure even after another context loss we don't get firings.
|
||||||
|
using (realmFactory.BlockAllOperations())
|
||||||
|
Assert.That(resolvedItems, Is.Null);
|
||||||
|
|
||||||
|
realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||||
|
|
||||||
|
testEventsArriving(false);
|
||||||
|
|
||||||
|
void testEventsArriving(bool shouldArrive)
|
||||||
|
{
|
||||||
|
realmFactory.Run(realm => realm.Refresh());
|
||||||
|
|
||||||
|
if (shouldArrive)
|
||||||
|
Assert.That(resolvedItems, Has.One.Items);
|
||||||
|
else
|
||||||
|
Assert.That(resolvedItems, Is.Null);
|
||||||
|
|
||||||
|
realmFactory.Write(realm =>
|
||||||
|
{
|
||||||
|
realm.RemoveAll<BeatmapSetInfo>();
|
||||||
|
realm.RemoveAll<RulesetInfo>();
|
||||||
|
});
|
||||||
|
|
||||||
|
realmFactory.Run(realm => realm.Refresh());
|
||||||
|
|
||||||
|
if (shouldArrive)
|
||||||
|
Assert.That(lastChanges?.DeletedIndices, Has.One.Items);
|
||||||
|
else
|
||||||
|
Assert.That(lastChanges, Is.Null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
|
||||||
|
{
|
||||||
|
if (changes == null)
|
||||||
|
resolvedItems = sender;
|
||||||
|
|
||||||
|
lastChanges = changes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCustomRegisterWithContextLoss()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
BeatmapSetInfo? beatmapSetInfo = null;
|
||||||
|
|
||||||
|
realmFactory.Write(realm => realm.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||||
|
|
||||||
|
var subscription = realmFactory.RegisterCustomSubscription(realm =>
|
||||||
|
{
|
||||||
|
beatmapSetInfo = realm.All<BeatmapSetInfo>().First();
|
||||||
|
|
||||||
|
return new InvokeOnDisposal(() => beatmapSetInfo = null);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.That(beatmapSetInfo, Is.Not.Null);
|
||||||
|
|
||||||
|
using (realmFactory.BlockAllOperations())
|
||||||
|
{
|
||||||
|
// custom disposal action fired when context lost.
|
||||||
|
Assert.That(beatmapSetInfo, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-registration after context restore.
|
||||||
|
realmFactory.Run(realm => realm.Refresh());
|
||||||
|
Assert.That(beatmapSetInfo, Is.Not.Null);
|
||||||
|
|
||||||
|
subscription.Dispose();
|
||||||
|
|
||||||
|
Assert.That(beatmapSetInfo, Is.Null);
|
||||||
|
|
||||||
|
using (realmFactory.BlockAllOperations())
|
||||||
|
Assert.That(beatmapSetInfo, Is.Null);
|
||||||
|
|
||||||
|
realmFactory.Run(realm => realm.Refresh());
|
||||||
|
Assert.That(beatmapSetInfo, Is.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
|
|
||||||
const int beat_length_numerator = 2000;
|
const int beat_length_numerator = 2000;
|
||||||
const int beat_length_denominator = 7;
|
const int beat_length_denominator = 7;
|
||||||
const TimeSignatures signature = TimeSignatures.SimpleQuadruple;
|
TimeSignature signature = TimeSignature.SimpleQuadruple;
|
||||||
|
|
||||||
var beatmap = new Beatmap
|
var beatmap = new Beatmap
|
||||||
{
|
{
|
||||||
@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
for (int i = 0; i * beat_length_denominator < barLines.Count; i++)
|
for (int i = 0; i * beat_length_denominator < barLines.Count; i++)
|
||||||
{
|
{
|
||||||
var barLine = barLines[i * beat_length_denominator];
|
var barLine = barLines[i * beat_length_denominator];
|
||||||
int expectedTime = beat_length_numerator * (int)signature * i;
|
int expectedTime = beat_length_numerator * signature.Numerator * i;
|
||||||
|
|
||||||
// every seventh bar's start time should be at least greater than the whole number we expect.
|
// every seventh bar's start time should be at least greater than the whole number we expect.
|
||||||
// It cannot be less, as that can affect overlapping scroll algorithms
|
// It cannot be less, as that can affect overlapping scroll algorithms
|
||||||
@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime));
|
Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime));
|
||||||
|
|
||||||
// check major/minor lines for good measure too
|
// check major/minor lines for good measure too
|
||||||
Assert.AreEqual(i % (int)signature == 0, barLine.Major);
|
Assert.AreEqual(i % signature.Numerator == 0, barLine.Major);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +80,10 @@ namespace osu.Game.Tests.Resources
|
|||||||
public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null)
|
public static BeatmapSetInfo CreateTestBeatmapSetInfo(int? difficultyCount = null, RulesetInfo[] rulesets = null)
|
||||||
{
|
{
|
||||||
int j = 0;
|
int j = 0;
|
||||||
RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length] ?? new OsuRuleset().RulesetInfo;
|
|
||||||
|
rulesets ??= new[] { new OsuRuleset().RulesetInfo };
|
||||||
|
|
||||||
|
RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length];
|
||||||
|
|
||||||
int setId = Interlocked.Increment(ref importId);
|
int setId = Interlocked.Increment(ref importId);
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
|
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
|
||||||
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
|
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
|
||||||
});
|
});
|
||||||
|
AddStep("Set author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username = "author");
|
||||||
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
|
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
|
||||||
|
|
||||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||||
@ -64,6 +65,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
|
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
|
||||||
|
|
||||||
checkMutations();
|
checkMutations();
|
||||||
|
AddAssert("Beatmap has correct .osu file path", () => editorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu");
|
||||||
|
|
||||||
AddStep("Exit", () => InputManager.Key(Key.Escape));
|
AddStep("Exit", () => InputManager.Key(Key.Escape));
|
||||||
|
|
||||||
@ -88,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
|
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
|
||||||
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
|
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
|
||||||
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
|
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
|
||||||
|
AddAssert("Beatmap has correct author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username == "author");
|
||||||
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
|
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Screens.Edit.Timing;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
public class TestSceneLabelledTimeSignature : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
private LabelledTimeSignature timeSignature;
|
||||||
|
|
||||||
|
private void createLabelledTimeSignature(TimeSignature initial) => AddStep("create labelled time signature", () =>
|
||||||
|
{
|
||||||
|
Child = timeSignature = new LabelledTimeSignature
|
||||||
|
{
|
||||||
|
Label = "Time Signature",
|
||||||
|
RelativeSizeAxes = Axes.None,
|
||||||
|
Width = 400,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Current = { Value = initial }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
private OsuTextBox numeratorTextBox => timeSignature.ChildrenOfType<OsuTextBox>().Single();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInitialValue()
|
||||||
|
{
|
||||||
|
createLabelledTimeSignature(TimeSignature.SimpleTriple);
|
||||||
|
AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeViaCurrent()
|
||||||
|
{
|
||||||
|
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("set current to 5/4", () => timeSignature.Current.Value = new TimeSignature(5));
|
||||||
|
|
||||||
|
AddAssert("current is 5/4", () => timeSignature.Current.Value.Equals(new TimeSignature(5)));
|
||||||
|
AddAssert("numerator is 5", () => numeratorTextBox.Current.Value == "5");
|
||||||
|
|
||||||
|
AddStep("set current to 3/4", () => timeSignature.Current.Value = TimeSignature.SimpleTriple);
|
||||||
|
|
||||||
|
AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple));
|
||||||
|
AddAssert("numerator is 3", () => numeratorTextBox.Current.Value == "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeNumerator()
|
||||||
|
{
|
||||||
|
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
|
||||||
|
|
||||||
|
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("drop focus", () => InputManager.ChangeFocus(null));
|
||||||
|
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInvalidChangeRollbackOnCommit()
|
||||||
|
{
|
||||||
|
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
|
||||||
|
|
||||||
|
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("drop focus", () => InputManager.ChangeFocus(null));
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
Add(new ModNightcore<HitObject>.NightcoreBeatContainer());
|
Add(new ModNightcore<HitObject>.NightcoreBeatContainer());
|
||||||
|
|
||||||
AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleQuadruple));
|
AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignature.SimpleQuadruple));
|
||||||
AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleTriple));
|
AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignature.SimpleTriple));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Overlays.Login;
|
using osu.Game.Overlays.Login;
|
||||||
|
using osu.Game.Users.Drawables;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Menus
|
namespace osu.Game.Tests.Visual.Menus
|
||||||
{
|
{
|
||||||
@ -15,6 +17,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
public class TestSceneLoginPanel : OsuManualInputManagerTestScene
|
public class TestSceneLoginPanel : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private LoginPanel loginPanel;
|
private LoginPanel loginPanel;
|
||||||
|
private int hideCount;
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
@ -26,6 +29,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
|
RequestHide = () => hideCount++,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -51,5 +55,22 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddStep("enter password", () => loginPanel.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
AddStep("enter password", () => loginPanel.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||||
AddStep("submit", () => loginPanel.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
AddStep("submit", () => loginPanel.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClickingOnFlagClosesPanel()
|
||||||
|
{
|
||||||
|
AddStep("reset hide count", () => hideCount = 0);
|
||||||
|
|
||||||
|
AddStep("logout", () => API.Logout());
|
||||||
|
AddStep("enter password", () => loginPanel.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||||
|
AddStep("submit", () => loginPanel.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||||
|
|
||||||
|
AddStep("click on flag", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(loginPanel.ChildrenOfType<UpdateableFlag>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddAssert("hide requested", () => hideCount == 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps
|
|||||||
private static string getFilename(BeatmapInfo beatmapInfo)
|
private static string getFilename(BeatmapInfo beatmapInfo)
|
||||||
{
|
{
|
||||||
var metadata = beatmapInfo.Metadata;
|
var metadata = beatmapInfo.Metadata;
|
||||||
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
|
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time signature at this control point.
|
/// The time signature at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Bindable<TimeSignatures> TimeSignatureBindable = new Bindable<TimeSignatures>(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
|
public readonly Bindable<TimeSignature> TimeSignatureBindable = new Bindable<TimeSignature>(TimeSignature.SimpleQuadruple);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
|
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
|
||||||
@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time signature at this control point.
|
/// The time signature at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSignatures TimeSignature
|
public TimeSignature TimeSignature
|
||||||
{
|
{
|
||||||
get => TimeSignatureBindable.Value;
|
get => TimeSignatureBindable.Value;
|
||||||
set => TimeSignatureBindable.Value = value;
|
set => TimeSignatureBindable.Value = value;
|
||||||
|
@ -340,9 +340,9 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
double beatLength = Parsing.ParseDouble(split[1].Trim());
|
double beatLength = Parsing.ParseDouble(split[1].Trim());
|
||||||
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
|
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
|
||||||
|
|
||||||
TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple;
|
TimeSignature timeSignature = TimeSignature.SimpleQuadruple;
|
||||||
if (split.Length >= 3)
|
if (split.Length >= 3)
|
||||||
timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]);
|
timeSignature = split[2][0] == '0' ? TimeSignature.SimpleQuadruple : new TimeSignature(Parsing.ParseInt(split[2]));
|
||||||
|
|
||||||
LegacySampleBank sampleSet = defaultSampleBank;
|
LegacySampleBank sampleSet = defaultSampleBank;
|
||||||
if (split.Length >= 4)
|
if (split.Length >= 4)
|
||||||
|
@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
if (effectPoint.OmitFirstBarLine)
|
if (effectPoint.OmitFirstBarLine)
|
||||||
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
|
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
|
||||||
|
|
||||||
writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},"));
|
writer.Write(FormattableString.Invariant($"{legacyControlPoints.TimingPointAt(time).TimeSignature.Numerator},"));
|
||||||
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
|
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
|
||||||
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
|
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
|
||||||
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
|
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
|
||||||
|
45
osu.Game/Beatmaps/Timing/TimeSignature.cs
Normal file
45
osu.Game/Beatmaps/Timing/TimeSignature.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps.Timing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the time signature of a track.
|
||||||
|
/// For now, the lower numeral can only be 4; support for other denominators can be considered at a later date.
|
||||||
|
/// </summary>
|
||||||
|
public class TimeSignature : IEquatable<TimeSignature>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The numerator of a signature.
|
||||||
|
/// </summary>
|
||||||
|
public int Numerator { get; }
|
||||||
|
|
||||||
|
// TODO: support time signatures with a denominator other than 4
|
||||||
|
// this in particular requires a new beatmap format.
|
||||||
|
|
||||||
|
public TimeSignature(int numerator)
|
||||||
|
{
|
||||||
|
if (numerator < 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(numerator), numerator, "The numerator of a time signature must be positive.");
|
||||||
|
|
||||||
|
Numerator = numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeSignature SimpleTriple { get; } = new TimeSignature(3);
|
||||||
|
public static TimeSignature SimpleQuadruple { get; } = new TimeSignature(4);
|
||||||
|
|
||||||
|
public override string ToString() => $"{Numerator}/4";
|
||||||
|
|
||||||
|
public bool Equals(TimeSignature other)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, other)) return false;
|
||||||
|
if (ReferenceEquals(this, other)) return true;
|
||||||
|
|
||||||
|
return Numerator == other.Numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() => Numerator;
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.Timing
|
namespace osu.Game.Beatmaps.Timing
|
||||||
{
|
{
|
||||||
public enum TimeSignatures
|
[Obsolete("Use osu.Game.Beatmaps.Timing.TimeSignature instead.")]
|
||||||
|
public enum TimeSignatures // can be removed 20220722
|
||||||
{
|
{
|
||||||
[Description("4/4")]
|
[Description("4/4")]
|
||||||
SimpleQuadruple = 4,
|
SimpleQuadruple = 4,
|
||||||
|
@ -27,7 +27,9 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
internal class EFToRealmMigrator : CompositeDrawable
|
internal class EFToRealmMigrator : CompositeDrawable
|
||||||
{
|
{
|
||||||
public bool FinishedMigrating { get; private set; }
|
public Task<bool> MigrationCompleted => migrationCompleted.Task;
|
||||||
|
|
||||||
|
private readonly TaskCompletionSource<bool> migrationCompleted = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private DatabaseContextFactory efContextFactory { get; set; } = null!;
|
private DatabaseContextFactory efContextFactory { get; set; } = null!;
|
||||||
@ -99,6 +101,17 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
using (var ef = efContextFactory.Get())
|
using (var ef = efContextFactory.Get())
|
||||||
{
|
{
|
||||||
|
realmContextFactory.Write(realm =>
|
||||||
|
{
|
||||||
|
// Before beginning, ensure realm is in an empty state.
|
||||||
|
// Migrations which are half-completed could lead to issues if the user tries a second time.
|
||||||
|
// Note that we only do this for beatmaps and scores since the other migrations are yonks old.
|
||||||
|
realm.RemoveAll<BeatmapSetInfo>();
|
||||||
|
realm.RemoveAll<BeatmapInfo>();
|
||||||
|
realm.RemoveAll<BeatmapMetadata>();
|
||||||
|
realm.RemoveAll<ScoreInfo>();
|
||||||
|
});
|
||||||
|
|
||||||
migrateSettings(ef);
|
migrateSettings(ef);
|
||||||
migrateSkins(ef);
|
migrateSkins(ef);
|
||||||
migrateBeatmaps(ef);
|
migrateBeatmaps(ef);
|
||||||
@ -114,7 +127,7 @@ namespace osu.Game.Database
|
|||||||
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
|
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
|
||||||
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||||
{
|
{
|
||||||
FinishedMigrating = true;
|
migrationCompleted.SetResult(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,87 +162,78 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
log($"Found {count} beatmaps in EF");
|
log($"Found {count} beatmaps in EF");
|
||||||
|
|
||||||
// only migrate data if the realm database is empty.
|
var transaction = realm.BeginWrite();
|
||||||
// note that this cannot be written as: `realm.All<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
int written = 0;
|
||||||
if (realm.All<BeatmapSetInfo>().Any(s => !s.Protected))
|
|
||||||
{
|
|
||||||
log("Skipping migration as realm already has beatmaps loaded");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var transaction = realm.BeginWrite();
|
|
||||||
int written = 0;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
foreach (var beatmapSet in existingBeatmapSets)
|
||||||
{
|
{
|
||||||
foreach (var beatmapSet in existingBeatmapSets)
|
if (++written % 1000 == 0)
|
||||||
{
|
{
|
||||||
if (++written % 1000 == 0)
|
transaction.Commit();
|
||||||
{
|
transaction = realm.BeginWrite();
|
||||||
transaction.Commit();
|
log($"Migrated {written}/{count} beatmaps...");
|
||||||
transaction = realm.BeginWrite();
|
}
|
||||||
log($"Migrated {written}/{count} beatmaps...");
|
|
||||||
}
|
|
||||||
|
|
||||||
var realmBeatmapSet = new BeatmapSetInfo
|
var realmBeatmapSet = new BeatmapSetInfo
|
||||||
|
{
|
||||||
|
OnlineID = beatmapSet.OnlineID ?? -1,
|
||||||
|
DateAdded = beatmapSet.DateAdded,
|
||||||
|
Status = beatmapSet.Status,
|
||||||
|
DeletePending = beatmapSet.DeletePending,
|
||||||
|
Hash = beatmapSet.Hash,
|
||||||
|
Protected = beatmapSet.Protected,
|
||||||
|
};
|
||||||
|
|
||||||
|
migrateFiles(beatmapSet, realm, realmBeatmapSet);
|
||||||
|
|
||||||
|
foreach (var beatmap in beatmapSet.Beatmaps)
|
||||||
|
{
|
||||||
|
var ruleset = realm.Find<RulesetInfo>(beatmap.RulesetInfo.ShortName);
|
||||||
|
var metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata);
|
||||||
|
|
||||||
|
var realmBeatmap = new BeatmapInfo(ruleset, new BeatmapDifficulty(beatmap.BaseDifficulty), metadata)
|
||||||
{
|
{
|
||||||
OnlineID = beatmapSet.OnlineID ?? -1,
|
DifficultyName = beatmap.DifficultyName,
|
||||||
DateAdded = beatmapSet.DateAdded,
|
Status = beatmap.Status,
|
||||||
Status = beatmapSet.Status,
|
OnlineID = beatmap.OnlineID ?? -1,
|
||||||
DeletePending = beatmapSet.DeletePending,
|
Length = beatmap.Length,
|
||||||
Hash = beatmapSet.Hash,
|
BPM = beatmap.BPM,
|
||||||
Protected = beatmapSet.Protected,
|
Hash = beatmap.Hash,
|
||||||
|
StarRating = beatmap.StarRating,
|
||||||
|
MD5Hash = beatmap.MD5Hash,
|
||||||
|
Hidden = beatmap.Hidden,
|
||||||
|
AudioLeadIn = beatmap.AudioLeadIn,
|
||||||
|
StackLeniency = beatmap.StackLeniency,
|
||||||
|
SpecialStyle = beatmap.SpecialStyle,
|
||||||
|
LetterboxInBreaks = beatmap.LetterboxInBreaks,
|
||||||
|
WidescreenStoryboard = beatmap.WidescreenStoryboard,
|
||||||
|
EpilepsyWarning = beatmap.EpilepsyWarning,
|
||||||
|
SamplesMatchPlaybackRate = beatmap.SamplesMatchPlaybackRate,
|
||||||
|
DistanceSpacing = beatmap.DistanceSpacing,
|
||||||
|
BeatDivisor = beatmap.BeatDivisor,
|
||||||
|
GridSize = beatmap.GridSize,
|
||||||
|
TimelineZoom = beatmap.TimelineZoom,
|
||||||
|
Countdown = beatmap.Countdown,
|
||||||
|
CountdownOffset = beatmap.CountdownOffset,
|
||||||
|
MaxCombo = beatmap.MaxCombo,
|
||||||
|
Bookmarks = beatmap.Bookmarks,
|
||||||
|
BeatmapSet = realmBeatmapSet,
|
||||||
};
|
};
|
||||||
|
|
||||||
migrateFiles(beatmapSet, realm, realmBeatmapSet);
|
realmBeatmapSet.Beatmaps.Add(realmBeatmap);
|
||||||
|
|
||||||
foreach (var beatmap in beatmapSet.Beatmaps)
|
|
||||||
{
|
|
||||||
var ruleset = realm.Find<RulesetInfo>(beatmap.RulesetInfo.ShortName);
|
|
||||||
var metadata = getBestMetadata(beatmap.Metadata, beatmapSet.Metadata);
|
|
||||||
|
|
||||||
var realmBeatmap = new BeatmapInfo(ruleset, new BeatmapDifficulty(beatmap.BaseDifficulty), metadata)
|
|
||||||
{
|
|
||||||
DifficultyName = beatmap.DifficultyName,
|
|
||||||
Status = beatmap.Status,
|
|
||||||
OnlineID = beatmap.OnlineID ?? -1,
|
|
||||||
Length = beatmap.Length,
|
|
||||||
BPM = beatmap.BPM,
|
|
||||||
Hash = beatmap.Hash,
|
|
||||||
StarRating = beatmap.StarRating,
|
|
||||||
MD5Hash = beatmap.MD5Hash,
|
|
||||||
Hidden = beatmap.Hidden,
|
|
||||||
AudioLeadIn = beatmap.AudioLeadIn,
|
|
||||||
StackLeniency = beatmap.StackLeniency,
|
|
||||||
SpecialStyle = beatmap.SpecialStyle,
|
|
||||||
LetterboxInBreaks = beatmap.LetterboxInBreaks,
|
|
||||||
WidescreenStoryboard = beatmap.WidescreenStoryboard,
|
|
||||||
EpilepsyWarning = beatmap.EpilepsyWarning,
|
|
||||||
SamplesMatchPlaybackRate = beatmap.SamplesMatchPlaybackRate,
|
|
||||||
DistanceSpacing = beatmap.DistanceSpacing,
|
|
||||||
BeatDivisor = beatmap.BeatDivisor,
|
|
||||||
GridSize = beatmap.GridSize,
|
|
||||||
TimelineZoom = beatmap.TimelineZoom,
|
|
||||||
Countdown = beatmap.Countdown,
|
|
||||||
CountdownOffset = beatmap.CountdownOffset,
|
|
||||||
MaxCombo = beatmap.MaxCombo,
|
|
||||||
Bookmarks = beatmap.Bookmarks,
|
|
||||||
BeatmapSet = realmBeatmapSet,
|
|
||||||
};
|
|
||||||
|
|
||||||
realmBeatmapSet.Beatmaps.Add(realmBeatmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
realm.Add(realmBeatmapSet);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
log($"Successfully migrated {count} beatmaps to realm");
|
realm.Add(realmBeatmapSet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
log($"Successfully migrated {count} beatmaps to realm");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,70 +284,62 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
log($"Found {count} scores in EF");
|
log($"Found {count} scores in EF");
|
||||||
|
|
||||||
// only migrate data if the realm database is empty.
|
var transaction = realm.BeginWrite();
|
||||||
if (realm.All<ScoreInfo>().Any())
|
int written = 0;
|
||||||
{
|
|
||||||
log("Skipping migration as realm already has scores loaded");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var transaction = realm.BeginWrite();
|
|
||||||
int written = 0;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
foreach (var score in existingScores)
|
||||||
{
|
{
|
||||||
foreach (var score in existingScores)
|
if (++written % 1000 == 0)
|
||||||
{
|
{
|
||||||
if (++written % 1000 == 0)
|
transaction.Commit();
|
||||||
{
|
transaction = realm.BeginWrite();
|
||||||
transaction.Commit();
|
log($"Migrated {written}/{count} scores...");
|
||||||
transaction = realm.BeginWrite();
|
|
||||||
log($"Migrated {written}/{count} scores...");
|
|
||||||
}
|
|
||||||
|
|
||||||
var beatmap = realm.All<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash);
|
|
||||||
var ruleset = realm.Find<RulesetInfo>(score.Ruleset.ShortName);
|
|
||||||
var user = new RealmUser
|
|
||||||
{
|
|
||||||
OnlineID = score.User.OnlineID,
|
|
||||||
Username = score.User.Username
|
|
||||||
};
|
|
||||||
|
|
||||||
var realmScore = new ScoreInfo(beatmap, ruleset, user)
|
|
||||||
{
|
|
||||||
Hash = score.Hash,
|
|
||||||
DeletePending = score.DeletePending,
|
|
||||||
OnlineID = score.OnlineID ?? -1,
|
|
||||||
ModsJson = score.ModsJson,
|
|
||||||
StatisticsJson = score.StatisticsJson,
|
|
||||||
TotalScore = score.TotalScore,
|
|
||||||
MaxCombo = score.MaxCombo,
|
|
||||||
Accuracy = score.Accuracy,
|
|
||||||
HasReplay = ((IScoreInfo)score).HasReplay,
|
|
||||||
Date = score.Date,
|
|
||||||
PP = score.PP,
|
|
||||||
Rank = score.Rank,
|
|
||||||
HitEvents = score.HitEvents,
|
|
||||||
Passed = score.Passed,
|
|
||||||
Combo = score.Combo,
|
|
||||||
Position = score.Position,
|
|
||||||
Statistics = score.Statistics,
|
|
||||||
Mods = score.Mods,
|
|
||||||
APIMods = score.APIMods,
|
|
||||||
};
|
|
||||||
|
|
||||||
migrateFiles(score, realm, realmScore);
|
|
||||||
|
|
||||||
realm.Add(realmScore);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
log($"Successfully migrated {count} scores to realm");
|
var beatmap = realm.All<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash);
|
||||||
|
var ruleset = realm.Find<RulesetInfo>(score.Ruleset.ShortName);
|
||||||
|
var user = new RealmUser
|
||||||
|
{
|
||||||
|
OnlineID = score.User.OnlineID,
|
||||||
|
Username = score.User.Username
|
||||||
|
};
|
||||||
|
|
||||||
|
var realmScore = new ScoreInfo(beatmap, ruleset, user)
|
||||||
|
{
|
||||||
|
Hash = score.Hash,
|
||||||
|
DeletePending = score.DeletePending,
|
||||||
|
OnlineID = score.OnlineID ?? -1,
|
||||||
|
ModsJson = score.ModsJson,
|
||||||
|
StatisticsJson = score.StatisticsJson,
|
||||||
|
TotalScore = score.TotalScore,
|
||||||
|
MaxCombo = score.MaxCombo,
|
||||||
|
Accuracy = score.Accuracy,
|
||||||
|
HasReplay = ((IScoreInfo)score).HasReplay,
|
||||||
|
Date = score.Date,
|
||||||
|
PP = score.PP,
|
||||||
|
Rank = score.Rank,
|
||||||
|
HitEvents = score.HitEvents,
|
||||||
|
Passed = score.Passed,
|
||||||
|
Combo = score.Combo,
|
||||||
|
Position = score.Position,
|
||||||
|
Statistics = score.Statistics,
|
||||||
|
Mods = score.Mods,
|
||||||
|
APIMods = score.APIMods,
|
||||||
|
};
|
||||||
|
|
||||||
|
migrateFiles(score, realm, realmScore);
|
||||||
|
|
||||||
|
realm.Add(realmScore);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
log($"Successfully migrated {count} scores to realm");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
46
osu.Game/Database/EmptyRealmSet.cs
Normal file
46
osu.Game/Database/EmptyRealmSet.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Realms;
|
||||||
|
using Realms.Schema;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public class EmptyRealmSet<T> : IRealmCollection<T>
|
||||||
|
{
|
||||||
|
private IList<T> emptySet => Array.Empty<T>();
|
||||||
|
|
||||||
|
public IEnumerator<T> GetEnumerator() => emptySet.GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator();
|
||||||
|
public int Count => emptySet.Count;
|
||||||
|
public T this[int index] => emptySet[index];
|
||||||
|
public int IndexOf(object item) => emptySet.IndexOf((T)item);
|
||||||
|
public bool Contains(object item) => emptySet.Contains((T)item);
|
||||||
|
|
||||||
|
public event NotifyCollectionChangedEventHandler? CollectionChanged
|
||||||
|
{
|
||||||
|
add => throw new NotImplementedException();
|
||||||
|
remove => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged
|
||||||
|
{
|
||||||
|
add => throw new NotImplementedException();
|
||||||
|
remove => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRealmCollection<T> Freeze() => throw new NotImplementedException();
|
||||||
|
public IDisposable SubscribeForNotifications(NotificationCallbackDelegate<T> callback) => throw new NotImplementedException();
|
||||||
|
public bool IsValid => throw new NotImplementedException();
|
||||||
|
public Realm Realm => throw new NotImplementedException();
|
||||||
|
public ObjectSchema ObjectSchema => throw new NotImplementedException();
|
||||||
|
public bool IsFrozen => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@ -61,33 +63,60 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
private readonly ThreadLocal<bool> currentThreadCanCreateContexts = new ThreadLocal<bool>();
|
private readonly ThreadLocal<bool> currentThreadCanCreateContexts = new ThreadLocal<bool>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds a map of functions registered via <see cref="RegisterCustomSubscription"/> and <see cref="RegisterForNotifications{T}"/> and a coinciding action which when triggered,
|
||||||
|
/// will unregister the subscription from realm.
|
||||||
|
///
|
||||||
|
/// Put another way, the key is an action which registers the subscription with realm. The returned <see cref="IDisposable"/> from the action is stored as the value and only
|
||||||
|
/// used internally.
|
||||||
|
///
|
||||||
|
/// Entries in this dictionary are only removed when a consumer signals that the subscription should be permanently ceased (via their own <see cref="IDisposable"/>).
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<Func<Realm, IDisposable?>, IDisposable?> customSubscriptionsResetMap = new Dictionary<Func<Realm, IDisposable?>, IDisposable?>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds a map of functions registered via <see cref="RegisterForNotifications{T}"/> and a coinciding action which when triggered,
|
||||||
|
/// fires a change set event with an empty collection. This is used to inform subscribers when a realm context goes away, and ensure they don't use invalidated
|
||||||
|
/// managed realm objects from a previous firing.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<Func<Realm, IDisposable?>, Action> notificationsResetMap = new Dictionary<Func<Realm, IDisposable?>, Action>();
|
||||||
|
|
||||||
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>(@"Realm", @"Contexts (Created)");
|
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>(@"Realm", @"Contexts (Created)");
|
||||||
|
|
||||||
private readonly object contextLock = new object();
|
private readonly object contextLock = new object();
|
||||||
|
|
||||||
private Realm? context;
|
private Realm? context;
|
||||||
|
|
||||||
public Realm Context
|
public Realm Context => ensureUpdateContext();
|
||||||
|
|
||||||
|
private Realm ensureUpdateContext()
|
||||||
{
|
{
|
||||||
get
|
if (!ThreadSafety.IsUpdateThread)
|
||||||
|
throw new InvalidOperationException(@$"Use {nameof(createContext)} when performing realm operations from a non-update thread");
|
||||||
|
|
||||||
|
lock (contextLock)
|
||||||
{
|
{
|
||||||
if (!ThreadSafety.IsUpdateThread)
|
if (context == null)
|
||||||
throw new InvalidOperationException(@$"Use {nameof(Run)}/{nameof(Write)} when performing realm operations from a non-update thread");
|
|
||||||
|
|
||||||
lock (contextLock)
|
|
||||||
{
|
{
|
||||||
if (context == null)
|
context = createContext();
|
||||||
{
|
Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}");
|
||||||
context = createContext();
|
|
||||||
Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// creating a context will ensure our schema is up-to-date and migrated.
|
// Resubscribe any subscriptions
|
||||||
return context;
|
foreach (var action in customSubscriptionsResetMap.Keys)
|
||||||
|
registerSubscription(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug.Assert(context != null);
|
||||||
|
|
||||||
|
// creating a context will ensure our schema is up-to-date and migrated.
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool CurrentThreadSubscriptionsAllowed => current_thread_subscriptions_allowed.Value;
|
||||||
|
|
||||||
|
private static readonly ThreadLocal<bool> current_thread_subscriptions_allowed = new ThreadLocal<bool>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Construct a new instance of a realm context factory.
|
/// Construct a new instance of a realm context factory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -222,6 +251,117 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribe to a realm collection and begin watching for asynchronous changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This adds osu! specific thread and managed state safety checks on top of <see cref="IRealmCollection{T}.SubscribeForNotifications"/>.
|
||||||
|
///
|
||||||
|
/// In addition to the documented realm behaviour, we have the additional requirement of handling subscriptions over potential context loss.
|
||||||
|
/// When this happens, callback events will be automatically fired:
|
||||||
|
/// - On context loss, a callback with an empty collection and <c>null</c> <see cref="ChangeSet"/> will be invoked.
|
||||||
|
/// - On context revival, a standard initial realm callback will arrive, with <c>null</c> <see cref="ChangeSet"/> and an up-to-date collection.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="query">The <see cref="IQueryable{T}"/> to observe for changes.</param>
|
||||||
|
/// <typeparam name="T">Type of the elements in the list.</typeparam>
|
||||||
|
/// <param name="callback">The callback to be invoked with the updated <see cref="IRealmCollection{T}"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
|
||||||
|
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
|
||||||
|
/// </returns>
|
||||||
|
/// <seealso cref="IRealmCollection{T}.SubscribeForNotifications"/>
|
||||||
|
public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback)
|
||||||
|
where T : RealmObjectBase
|
||||||
|
{
|
||||||
|
if (!ThreadSafety.IsUpdateThread)
|
||||||
|
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
|
||||||
|
|
||||||
|
lock (contextLock)
|
||||||
|
{
|
||||||
|
Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback);
|
||||||
|
|
||||||
|
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
|
||||||
|
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null, null));
|
||||||
|
return RegisterCustomSubscription(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run work on realm that will be run every time the update thread realm context gets recycled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">The work to run. Return value should be an <see cref="IDisposable"/> from QueryAsyncWithNotifications, or an <see cref="InvokeOnDisposal"/> to clean up any bindings.</param>
|
||||||
|
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
|
||||||
|
public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> action)
|
||||||
|
{
|
||||||
|
if (!ThreadSafety.IsUpdateThread)
|
||||||
|
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
|
||||||
|
|
||||||
|
var syncContext = SynchronizationContext.Current;
|
||||||
|
|
||||||
|
registerSubscription(action);
|
||||||
|
|
||||||
|
// This token is returned to the consumer.
|
||||||
|
// When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class).
|
||||||
|
return new InvokeOnDisposal(() =>
|
||||||
|
{
|
||||||
|
if (ThreadSafety.IsUpdateThread)
|
||||||
|
unsubscribe();
|
||||||
|
else
|
||||||
|
syncContext.Post(_ => unsubscribe(), null);
|
||||||
|
|
||||||
|
void unsubscribe()
|
||||||
|
{
|
||||||
|
lock (contextLock)
|
||||||
|
{
|
||||||
|
if (customSubscriptionsResetMap.TryGetValue(action, out var unsubscriptionAction))
|
||||||
|
{
|
||||||
|
unsubscriptionAction?.Dispose();
|
||||||
|
customSubscriptionsResetMap.Remove(action);
|
||||||
|
notificationsResetMap.Remove(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerSubscription(Func<Realm, IDisposable?> action)
|
||||||
|
{
|
||||||
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
|
lock (contextLock)
|
||||||
|
{
|
||||||
|
// Retrieve context outside of flag update to ensure that the context is constructed,
|
||||||
|
// as attempting to access it inside the subscription if it's not constructed would lead to
|
||||||
|
// cyclic invocations of the subscription callback.
|
||||||
|
var realm = Context;
|
||||||
|
|
||||||
|
Debug.Assert(!customSubscriptionsResetMap.TryGetValue(action, out var found) || found == null);
|
||||||
|
|
||||||
|
current_thread_subscriptions_allowed.Value = true;
|
||||||
|
customSubscriptionsResetMap[action] = action(realm);
|
||||||
|
current_thread_subscriptions_allowed.Value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unregister all subscriptions when the realm context is to be recycled.
|
||||||
|
/// Subscriptions will still remain and will be re-subscribed when the realm context returns.
|
||||||
|
/// </summary>
|
||||||
|
private void unregisterAllSubscriptions()
|
||||||
|
{
|
||||||
|
lock (contextLock)
|
||||||
|
{
|
||||||
|
foreach (var action in notificationsResetMap.Values)
|
||||||
|
action();
|
||||||
|
|
||||||
|
foreach (var action in customSubscriptionsResetMap)
|
||||||
|
{
|
||||||
|
action.Value?.Dispose();
|
||||||
|
customSubscriptionsResetMap[action.Key] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Realm createContext()
|
private Realm createContext()
|
||||||
{
|
{
|
||||||
if (isDisposed)
|
if (isDisposed)
|
||||||
@ -454,14 +594,29 @@ namespace osu.Game.Database
|
|||||||
if (isDisposed)
|
if (isDisposed)
|
||||||
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
||||||
|
|
||||||
|
SynchronizationContext? syncContext = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
contextCreationLock.Wait();
|
contextCreationLock.Wait();
|
||||||
|
|
||||||
lock (contextLock)
|
lock (contextLock)
|
||||||
{
|
{
|
||||||
if (!ThreadSafety.IsUpdateThread && context != null)
|
if (context == null)
|
||||||
throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
|
{
|
||||||
|
// null context means the update thread has not yet retrieved its context.
|
||||||
|
// we don't need to worry about reviving the update context in this case, so don't bother with the SynchronizationContext.
|
||||||
|
Debug.Assert(!ThreadSafety.IsUpdateThread);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ThreadSafety.IsUpdateThread)
|
||||||
|
throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
|
||||||
|
|
||||||
|
syncContext = SynchronizationContext.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterAllSubscriptions();
|
||||||
|
|
||||||
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
|
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
|
||||||
|
|
||||||
@ -501,6 +656,9 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
factory.contextCreationLock.Release();
|
factory.contextCreationLock.Release();
|
||||||
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
|
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
|
||||||
|
|
||||||
|
// Post back to the update thread to revive any subscriptions.
|
||||||
|
syncContext?.Post(_ => ensureUpdateContext(), null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ using System.Linq;
|
|||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.Internal;
|
using AutoMapper.Internal;
|
||||||
using osu.Framework.Development;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
@ -272,9 +271,8 @@ namespace osu.Game.Database
|
|||||||
public static IDisposable? QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
|
public static IDisposable? QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
|
||||||
where T : RealmObjectBase
|
where T : RealmObjectBase
|
||||||
{
|
{
|
||||||
// Subscriptions can only work on the main thread.
|
if (!RealmContextFactory.CurrentThreadSubscriptionsAllowed)
|
||||||
if (!ThreadSafety.IsUpdateThread)
|
throw new InvalidOperationException($"Make sure to call {nameof(RealmContextFactory)}.{nameof(RealmContextFactory.RegisterForNotifications)}");
|
||||||
throw new InvalidOperationException("Cannot subscribe for realm notifications from a non-update thread.");
|
|
||||||
|
|
||||||
return collection.SubscribeForNotifications(callback);
|
return collection.SubscribeForNotifications(callback);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ namespace osu.Game.Input.Bindings
|
|||||||
private readonly int? variant;
|
private readonly int? variant;
|
||||||
|
|
||||||
private IDisposable realmSubscription;
|
private IDisposable realmSubscription;
|
||||||
private IQueryable<RealmKeyBinding> realmKeyBindings;
|
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private RealmContextFactory realmFactory { get; set; }
|
private RealmContextFactory realmFactory { get; set; }
|
||||||
@ -47,22 +46,21 @@ namespace osu.Game.Input.Bindings
|
|||||||
throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided.");
|
throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
private IQueryable<RealmKeyBinding> queryRealmKeyBindings()
|
||||||
{
|
{
|
||||||
string rulesetName = ruleset?.ShortName;
|
string rulesetName = ruleset?.ShortName;
|
||||||
|
return realmFactory.Context.All<RealmKeyBinding>()
|
||||||
|
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
|
||||||
|
}
|
||||||
|
|
||||||
realmKeyBindings = realmFactory.Context.All<RealmKeyBinding>()
|
protected override void LoadComplete()
|
||||||
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
|
{
|
||||||
|
realmSubscription = realmFactory.RegisterForNotifications(realm => queryRealmKeyBindings(), (sender, changes, error) =>
|
||||||
realmSubscription = realmKeyBindings
|
{
|
||||||
.QueryAsyncWithNotifications((sender, changes, error) =>
|
// The first fire of this is a bit redundant as this is being called in base.LoadComplete,
|
||||||
{
|
// but this is safest in case the subscription is restored after a context recycle.
|
||||||
// first subscription ignored as we are handling this in LoadComplete.
|
ReloadMappings();
|
||||||
if (changes == null)
|
});
|
||||||
return;
|
|
||||||
|
|
||||||
ReloadMappings();
|
|
||||||
});
|
|
||||||
|
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
}
|
}
|
||||||
@ -78,11 +76,11 @@ namespace osu.Game.Input.Bindings
|
|||||||
{
|
{
|
||||||
var defaults = DefaultKeyBindings.ToList();
|
var defaults = DefaultKeyBindings.ToList();
|
||||||
|
|
||||||
List<RealmKeyBinding> newBindings = realmKeyBindings.Detach()
|
List<RealmKeyBinding> newBindings = queryRealmKeyBindings().Detach()
|
||||||
// this ordering is important to ensure that we read entries from the database in the order
|
// this ordering is important to ensure that we read entries from the database in the order
|
||||||
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
||||||
// have been eaten by the music controller due to query order.
|
// have been eaten by the music controller due to query order.
|
||||||
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList();
|
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList();
|
||||||
|
|
||||||
// In the case no bindings were found in the database, presume this usage is for a non-databased ruleset.
|
// In the case no bindings were found in the database, presume this usage is for a non-databased ruleset.
|
||||||
// This actually should never be required and can be removed if it is ever deemed to cause a problem.
|
// This actually should never be required and can be removed if it is ever deemed to cause a problem.
|
||||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Online
|
|||||||
// Used to interact with manager classes that don't support interface types. Will eventually be replaced.
|
// Used to interact with manager classes that don't support interface types. Will eventually be replaced.
|
||||||
var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID };
|
var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID };
|
||||||
|
|
||||||
realmSubscription = realmContextFactory.Context.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) =>
|
realmSubscription = realmContextFactory.RegisterForNotifications(realm => realm.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, changes, ___) =>
|
||||||
{
|
{
|
||||||
if (items.Any())
|
if (items.Any())
|
||||||
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
||||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Online.Rooms
|
|||||||
|
|
||||||
// handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow).
|
// handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow).
|
||||||
realmSubscription?.Dispose();
|
realmSubscription?.Dispose();
|
||||||
realmSubscription = filteredBeatmaps().QueryAsyncWithNotifications((items, changes, ___) =>
|
realmSubscription = realmContextFactory.RegisterForNotifications(realm => filteredBeatmaps(), (items, changes, ___) =>
|
||||||
{
|
{
|
||||||
if (changes == null)
|
if (changes == null)
|
||||||
return;
|
return;
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Online
|
|||||||
Downloader.DownloadBegan += downloadBegan;
|
Downloader.DownloadBegan += downloadBegan;
|
||||||
Downloader.DownloadFailed += downloadFailed;
|
Downloader.DownloadFailed += downloadFailed;
|
||||||
|
|
||||||
realmSubscription = realmContextFactory.Context.All<ScoreInfo>().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending).QueryAsyncWithNotifications((items, changes, ___) =>
|
realmSubscription = realmContextFactory.RegisterForNotifications(realm => realm.All<ScoreInfo>().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) || s.Hash == TrackedItem.Hash) && !s.DeletePending), (items, changes, ___) =>
|
||||||
{
|
{
|
||||||
if (items.Any())
|
if (items.Any())
|
||||||
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
Schedule(() => UpdateState(DownloadState.LocallyAvailable));
|
||||||
|
@ -30,16 +30,7 @@ namespace osu.Game.Overlays
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapManager beatmaps { get; set; }
|
private BeatmapManager beatmaps { get; set; }
|
||||||
|
|
||||||
public IBindableList<BeatmapSetInfo> BeatmapSets
|
public IBindableList<BeatmapSetInfo> BeatmapSets => beatmapSets;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (LoadState < LoadState.Ready)
|
|
||||||
throw new InvalidOperationException($"{nameof(BeatmapSets)} should not be accessed before the music controller is loaded.");
|
|
||||||
|
|
||||||
return beatmapSets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Point in time after which the current track will be restarted on triggering a "previous track" action.
|
/// Point in time after which the current track will be restarted on triggering a "previous track" action.
|
||||||
@ -80,26 +71,26 @@ namespace osu.Game.Overlays
|
|||||||
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
|
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IQueryable<BeatmapSetInfo> queryRealmBeatmapSets() =>
|
||||||
|
realmFactory.Context
|
||||||
|
.All<BeatmapSetInfo>()
|
||||||
|
.Where(s => !s.DeletePending);
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
beatmapSubscription = realmFactory.RegisterForNotifications(realm => queryRealmBeatmapSets(), beatmapsChanged);
|
||||||
var availableBeatmaps = realmFactory.Context
|
|
||||||
.All<BeatmapSetInfo>()
|
|
||||||
.Where(s => !s.DeletePending);
|
|
||||||
|
|
||||||
// ensure we're ready before completing async load.
|
|
||||||
// probably not a good way of handling this (as there is a period we aren't watching for changes until the realm subscription finishes up.
|
|
||||||
foreach (var s in availableBeatmaps)
|
|
||||||
beatmapSets.Add(s);
|
|
||||||
|
|
||||||
beatmapSubscription = availableBeatmaps.QueryAsyncWithNotifications(beatmapsChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
|
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
|
||||||
{
|
{
|
||||||
if (changes == null)
|
if (changes == null)
|
||||||
|
{
|
||||||
|
beatmapSets.Clear();
|
||||||
|
foreach (var s in sender)
|
||||||
|
beatmapSets.Add(s.Detach());
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (int i in changes.InsertedIndices)
|
foreach (int i in changes.InsertedIndices)
|
||||||
beatmapSets.Insert(i, sender[i].Detach());
|
beatmapSets.Insert(i, sender[i].Detach());
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
@ -17,6 +21,9 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, RealmContextFactory realmFactory)
|
private void load(GameHost host, RealmContextFactory realmFactory)
|
||||||
{
|
{
|
||||||
|
SettingsButton blockAction;
|
||||||
|
SettingsButton unblockAction;
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new SettingsButton
|
new SettingsButton
|
||||||
@ -35,6 +42,51 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
blockAction = new SettingsButton
|
||||||
|
{
|
||||||
|
Text = "Block realm",
|
||||||
|
},
|
||||||
|
unblockAction = new SettingsButton
|
||||||
|
{
|
||||||
|
Text = "Unblock realm",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
blockAction.Action = () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = realmFactory.BlockAllOperations();
|
||||||
|
|
||||||
|
blockAction.Enabled.Value = false;
|
||||||
|
|
||||||
|
// As a safety measure, unblock after 10 seconds.
|
||||||
|
// This is to handle the case where a dev may block, but then something on the update thread
|
||||||
|
// accesses realm and blocks for eternity.
|
||||||
|
Task.Factory.StartNew(() =>
|
||||||
|
{
|
||||||
|
Thread.Sleep(10000);
|
||||||
|
unblock();
|
||||||
|
});
|
||||||
|
|
||||||
|
unblockAction.Action = unblock;
|
||||||
|
|
||||||
|
void unblock()
|
||||||
|
{
|
||||||
|
token?.Dispose();
|
||||||
|
token = null;
|
||||||
|
|
||||||
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
blockAction.Enabled.Value = true;
|
||||||
|
unblockAction.Action = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error(e, "Blocking realm failed");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,12 @@ namespace osu.Game.Overlays.Settings.Sections
|
|||||||
private RealmContextFactory realmFactory { get; set; }
|
private RealmContextFactory realmFactory { get; set; }
|
||||||
|
|
||||||
private IDisposable realmSubscription;
|
private IDisposable realmSubscription;
|
||||||
private IQueryable<SkinInfo> realmSkins;
|
|
||||||
|
private IQueryable<SkinInfo> queryRealmSkins() =>
|
||||||
|
realmFactory.Context.All<SkinInfo>()
|
||||||
|
.Where(s => !s.DeletePending)
|
||||||
|
.OrderByDescending(s => s.Protected) // protected skins should be at the top.
|
||||||
|
.ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
[BackgroundDependencyLoader(permitNulls: true)]
|
[BackgroundDependencyLoader(permitNulls: true)]
|
||||||
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
|
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
|
||||||
@ -78,20 +83,12 @@ namespace osu.Game.Overlays.Settings.Sections
|
|||||||
|
|
||||||
skinDropdown.Current = dropdownBindable;
|
skinDropdown.Current = dropdownBindable;
|
||||||
|
|
||||||
realmSkins = realmFactory.Context.All<SkinInfo>()
|
realmSubscription = realmFactory.RegisterForNotifications(realm => queryRealmSkins(), (sender, changes, error) =>
|
||||||
.Where(s => !s.DeletePending)
|
{
|
||||||
.OrderByDescending(s => s.Protected) // protected skins should be at the top.
|
// The first fire of this is a bit redundant due to the call below,
|
||||||
.ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase);
|
// but this is safest in case the subscription is restored after a context recycle.
|
||||||
|
updateItems();
|
||||||
realmSubscription = realmSkins
|
});
|
||||||
.QueryAsyncWithNotifications((sender, changes, error) =>
|
|
||||||
{
|
|
||||||
if (changes == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Eventually this should be handling the individual changes rather than refreshing the whole dropdown.
|
|
||||||
updateItems();
|
|
||||||
});
|
|
||||||
|
|
||||||
updateItems();
|
updateItems();
|
||||||
|
|
||||||
@ -131,9 +128,9 @@ namespace osu.Game.Overlays.Settings.Sections
|
|||||||
|
|
||||||
private void updateItems()
|
private void updateItems()
|
||||||
{
|
{
|
||||||
int protectedCount = realmSkins.Count(s => s.Protected);
|
int protectedCount = queryRealmSkins().Count(s => s.Protected);
|
||||||
|
|
||||||
skinItems = realmSkins.ToLive(realmFactory);
|
skinItems = queryRealmSkins().ToLive(realmFactory);
|
||||||
|
|
||||||
skinItems.Insert(protectedCount, random_skin_info);
|
skinItems.Insert(protectedCount, random_skin_info);
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
|
|
||||||
if (!IsBeatSyncedWithTrack) return;
|
if (!IsBeatSyncedWithTrack) return;
|
||||||
|
|
||||||
int timeSignature = (int)timingPoint.TimeSignature;
|
int timeSignature = timingPoint.TimeSignature.Numerator;
|
||||||
|
|
||||||
// play metronome from one measure before the first object.
|
// play metronome from one measure before the first object.
|
||||||
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
|
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
|
||||||
|
@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
{
|
{
|
||||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||||
|
|
||||||
int beatsPerBar = (int)timingPoint.TimeSignature;
|
int beatsPerBar = timingPoint.TimeSignature.Numerator;
|
||||||
int segmentLength = beatsPerBar * Divisor * bars_per_segment;
|
int segmentLength = beatsPerBar * Divisor * bars_per_segment;
|
||||||
|
|
||||||
if (!IsBeatSyncedWithTrack)
|
if (!IsBeatSyncedWithTrack)
|
||||||
@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature);
|
playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playBeatFor(int beatIndex, TimeSignatures signature)
|
private void playBeatFor(int beatIndex, TimeSignature signature)
|
||||||
{
|
{
|
||||||
if (beatIndex == 0)
|
if (beatIndex == 0)
|
||||||
finishSample?.Play();
|
finishSample?.Play();
|
||||||
|
|
||||||
switch (signature)
|
switch (signature.Numerator)
|
||||||
{
|
{
|
||||||
case TimeSignatures.SimpleTriple:
|
case 3:
|
||||||
switch (beatIndex % 6)
|
switch (beatIndex % 6)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TimeSignatures.SimpleQuadruple:
|
case 4:
|
||||||
switch (beatIndex % 4)
|
switch (beatIndex % 4)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
int currentBeat = 0;
|
int currentBeat = 0;
|
||||||
|
|
||||||
// Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
|
// Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
|
||||||
double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature;
|
double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
|
||||||
|
|
||||||
double barLength = currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature;
|
double barLength = currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
|
||||||
|
|
||||||
for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
|
for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
|
||||||
{
|
{
|
||||||
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
BarLines.Add(new TBarLine
|
BarLines.Add(new TBarLine
|
||||||
{
|
{
|
||||||
StartTime = t,
|
StartTime = t,
|
||||||
Major = currentBeat % (int)currentTimingPoint.TimeSignature == 0
|
Major = currentBeat % currentTimingPoint.TimeSignature.Numerator == 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
if (beat == 0 && i == 0)
|
if (beat == 0 && i == 0)
|
||||||
nextMinTick = float.MinValue;
|
nextMinTick = float.MinValue;
|
||||||
|
|
||||||
int indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
|
int indexInBar = beat % (point.TimeSignature.Numerator * beatDivisor.Value);
|
||||||
|
|
||||||
int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
|
int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
|
||||||
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
|
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
|
||||||
|
97
osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs
Normal file
97
osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Timing
|
||||||
|
{
|
||||||
|
public class LabelledTimeSignature : LabelledComponent<LabelledTimeSignature.TimeSignatureBox, TimeSignature>
|
||||||
|
{
|
||||||
|
public LabelledTimeSignature()
|
||||||
|
: base(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TimeSignatureBox CreateComponent() => new TimeSignatureBox();
|
||||||
|
|
||||||
|
public class TimeSignatureBox : CompositeDrawable, IHasCurrentValue<TimeSignature>
|
||||||
|
{
|
||||||
|
private readonly BindableWithCurrent<TimeSignature> current = new BindableWithCurrent<TimeSignature>(TimeSignature.SimpleQuadruple);
|
||||||
|
|
||||||
|
public Bindable<TimeSignature> Current
|
||||||
|
{
|
||||||
|
get => current.Current;
|
||||||
|
set => current.Current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OsuNumberBox numeratorBox;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
InternalChild = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
numeratorBox = new OsuNumberBox
|
||||||
|
{
|
||||||
|
Width = 40,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
CornerRadius = CORNER_RADIUS,
|
||||||
|
CommitOnFocusLost = true
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Margin = new MarginPadding
|
||||||
|
{
|
||||||
|
Left = 5,
|
||||||
|
Right = CONTENT_PADDING_HORIZONTAL
|
||||||
|
},
|
||||||
|
Text = "/ 4",
|
||||||
|
Font = OsuFont.Default.With(size: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
Current.BindValueChanged(_ => updateFromCurrent(), true);
|
||||||
|
numeratorBox.OnCommit += (_, __) => updateFromNumeratorBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFromCurrent()
|
||||||
|
{
|
||||||
|
numeratorBox.Current.Value = Current.Value.Numerator.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFromNumeratorBox()
|
||||||
|
{
|
||||||
|
if (int.TryParse(numeratorBox.Current.Value, out int numerator) && numerator > 0)
|
||||||
|
Current.Value = new TimeSignature(numerator);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// trigger `Current` change to restore the numerator box's text to a valid value.
|
||||||
|
Current.TriggerChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
|||||||
public class TimingRowAttribute : RowAttribute
|
public class TimingRowAttribute : RowAttribute
|
||||||
{
|
{
|
||||||
private readonly BindableNumber<double> beatLength;
|
private readonly BindableNumber<double> beatLength;
|
||||||
private readonly Bindable<TimeSignatures> timeSignature;
|
private readonly Bindable<TimeSignature> timeSignature;
|
||||||
private OsuSpriteText text;
|
private OsuSpriteText text;
|
||||||
|
|
||||||
public TimingRowAttribute(TimingControlPoint timing)
|
public TimingRowAttribute(TimingControlPoint timing)
|
||||||
|
@ -6,7 +6,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Beatmaps.Timing;
|
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
internal class TimingSection : Section<TimingControlPoint>
|
internal class TimingSection : Section<TimingControlPoint>
|
||||||
{
|
{
|
||||||
private SettingsSlider<double> bpmSlider;
|
private SettingsSlider<double> bpmSlider;
|
||||||
private SettingsEnumDropdown<TimeSignatures> timeSignature;
|
private LabelledTimeSignature timeSignature;
|
||||||
private BPMTextBox bpmTextEntry;
|
private BPMTextBox bpmTextEntry;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -25,10 +24,10 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
{
|
{
|
||||||
bpmTextEntry = new BPMTextBox(),
|
bpmTextEntry = new BPMTextBox(),
|
||||||
bpmSlider = new BPMSlider(),
|
bpmSlider = new BPMSlider(),
|
||||||
timeSignature = new SettingsEnumDropdown<TimeSignatures>
|
timeSignature = new LabelledTimeSignature
|
||||||
{
|
{
|
||||||
LabelText = "Time Signature"
|
Label = "Time Signature"
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,11 +74,22 @@ namespace osu.Game.Screens
|
|||||||
base.OnEntering(last);
|
base.OnEntering(last);
|
||||||
|
|
||||||
LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal);
|
LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal);
|
||||||
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
|
||||||
|
|
||||||
// A non-null context factory means there's still content to migrate.
|
// A non-null context factory means there's still content to migrate.
|
||||||
if (efContextFactory != null)
|
if (efContextFactory != null)
|
||||||
|
{
|
||||||
LoadComponentAsync(realmMigrator = new EFToRealmMigrator(), AddInternal);
|
LoadComponentAsync(realmMigrator = new EFToRealmMigrator(), AddInternal);
|
||||||
|
realmMigrator.MigrationCompleted.ContinueWith(_ => Schedule(() =>
|
||||||
|
{
|
||||||
|
// Delay initial screen loading to ensure that the migration is in a complete and sane state
|
||||||
|
// before the intro screen may import the game intro beatmap.
|
||||||
|
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
||||||
|
}
|
||||||
|
|
||||||
LoadComponentAsync(spinner = new LoadingSpinner(true, true)
|
LoadComponentAsync(spinner = new LoadingSpinner(true, true)
|
||||||
{
|
{
|
||||||
@ -96,7 +107,7 @@ namespace osu.Game.Screens
|
|||||||
|
|
||||||
private void checkIfLoaded()
|
private void checkIfLoaded()
|
||||||
{
|
{
|
||||||
if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling || realmMigrator?.FinishedMigrating == false)
|
if (loadableScreen?.LoadState != LoadState.Ready || !precompiler.FinishedCompiling)
|
||||||
{
|
{
|
||||||
Schedule(checkIfLoaded);
|
Schedule(checkIfLoaded);
|
||||||
return;
|
return;
|
||||||
|
@ -21,6 +21,7 @@ using osu.Game.Overlays;
|
|||||||
using osu.Game.Screens.Backgrounds;
|
using osu.Game.Screens.Backgrounds;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Menu
|
namespace osu.Game.Screens.Menu
|
||||||
{
|
{
|
||||||
@ -93,28 +94,27 @@ namespace osu.Game.Screens.Menu
|
|||||||
MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
|
MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
|
||||||
seeya = audio.Samples.Get(SeeyaSampleName);
|
seeya = audio.Samples.Get(SeeyaSampleName);
|
||||||
|
|
||||||
ILive<BeatmapSetInfo> setInfo = null;
|
|
||||||
|
|
||||||
// if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection.
|
// if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection.
|
||||||
if (!MenuMusic.Value)
|
if (!MenuMusic.Value)
|
||||||
{
|
{
|
||||||
var sets = beatmaps.GetAllUsableBeatmapSets();
|
realmContextFactory.Run(realm =>
|
||||||
|
|
||||||
if (sets.Count > 0)
|
|
||||||
{
|
{
|
||||||
setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID);
|
var usableBeatmapSets = realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection();
|
||||||
setInfo?.PerformRead(s =>
|
|
||||||
{
|
|
||||||
if (s.Beatmaps.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]);
|
int setCount = usableBeatmapSets.Count;
|
||||||
});
|
|
||||||
}
|
if (setCount > 0)
|
||||||
|
{
|
||||||
|
var found = usableBeatmapSets[RNG.Next(0, setCount - 1)].Beatmaps.FirstOrDefault();
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
initialBeatmap = beatmaps.GetWorkingBeatmap(found);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available.
|
// we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available.
|
||||||
if (setInfo == null)
|
if (initialBeatmap == null)
|
||||||
{
|
{
|
||||||
if (!loadThemedIntro())
|
if (!loadThemedIntro())
|
||||||
{
|
{
|
||||||
@ -130,7 +130,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
bool loadThemedIntro()
|
bool loadThemedIntro()
|
||||||
{
|
{
|
||||||
setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash);
|
var setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash);
|
||||||
|
|
||||||
if (setInfo == null)
|
if (setInfo == null)
|
||||||
return false;
|
return false;
|
||||||
|
@ -94,9 +94,9 @@ namespace osu.Game.Screens.Menu
|
|||||||
if (beatIndex < 0)
|
if (beatIndex < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % (int)timingPoint.TimeSignature == 0)
|
if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % timingPoint.TimeSignature.Numerator == 0)
|
||||||
flash(leftBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes);
|
flash(leftBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes);
|
||||||
if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % (int)timingPoint.TimeSignature == 0)
|
if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % timingPoint.TimeSignature.Numerator == 0)
|
||||||
flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes);
|
flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
{
|
{
|
||||||
this.Delay(early_activation).Schedule(() =>
|
this.Delay(early_activation).Schedule(() =>
|
||||||
{
|
{
|
||||||
if (beatIndex % (int)timingPoint.TimeSignature == 0)
|
if (beatIndex % timingPoint.TimeSignature.Numerator == 0)
|
||||||
sampleDownbeat.Play();
|
sampleDownbeat.Play();
|
||||||
else
|
else
|
||||||
sampleBeat.Play();
|
sampleBeat.Play();
|
||||||
|
@ -190,13 +190,13 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
subscriptionSets = getBeatmapSets(realmFactory.Context).QueryAsyncWithNotifications(beatmapSetsChanged);
|
subscriptionSets = realmFactory.RegisterForNotifications(getBeatmapSets, beatmapSetsChanged);
|
||||||
subscriptionBeatmaps = realmFactory.Context.All<BeatmapInfo>().Where(b => !b.Hidden).QueryAsyncWithNotifications(beatmapsChanged);
|
subscriptionBeatmaps = realmFactory.RegisterForNotifications(realm => realm.All<BeatmapInfo>().Where(b => !b.Hidden), beatmapsChanged);
|
||||||
|
|
||||||
// Can't use main subscriptions because we can't lookup deleted indices.
|
// Can't use main subscriptions because we can't lookup deleted indices.
|
||||||
// https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595.
|
// https://github.com/realm/realm-dotnet/discussions/2634#discussioncomment-1605595.
|
||||||
subscriptionDeletedSets = realmFactory.Context.All<BeatmapSetInfo>().Where(s => s.DeletePending && !s.Protected).QueryAsyncWithNotifications(deletedBeatmapSetsChanged);
|
subscriptionDeletedSets = realmFactory.RegisterForNotifications(realm => realm.All<BeatmapSetInfo>().Where(s => s.DeletePending && !s.Protected), deletedBeatmapSetsChanged);
|
||||||
subscriptionHiddenBeatmaps = realmFactory.Context.All<BeatmapInfo>().Where(b => b.Hidden).QueryAsyncWithNotifications(beatmapsChanged);
|
subscriptionHiddenBeatmaps = realmFactory.RegisterForNotifications(realm => realm.All<BeatmapInfo>().Where(b => b.Hidden), beatmapsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deletedBeatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
|
private void deletedBeatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
|
||||||
@ -274,7 +274,7 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IRealmCollection<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection();
|
private IQueryable<BeatmapSetInfo> getBeatmapSets(Realm realm) => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
|
||||||
|
|
||||||
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) =>
|
public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) =>
|
||||||
removeBeatmapSet(beatmapSet.ID);
|
removeBeatmapSet(beatmapSet.ID);
|
||||||
@ -552,10 +552,11 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private void signalBeatmapsLoaded()
|
private void signalBeatmapsLoaded()
|
||||||
{
|
{
|
||||||
Debug.Assert(BeatmapSetsLoaded == false);
|
if (!BeatmapSetsLoaded)
|
||||||
|
{
|
||||||
BeatmapSetsChanged?.Invoke();
|
BeatmapSetsChanged?.Invoke();
|
||||||
BeatmapSetsLoaded = true;
|
BeatmapSetsLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
itemsCache.Invalidate();
|
itemsCache.Invalidate();
|
||||||
}
|
}
|
||||||
|
@ -48,18 +48,19 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
ruleset.BindValueChanged(_ =>
|
ruleset.BindValueChanged(_ =>
|
||||||
{
|
{
|
||||||
scoreSubscription?.Dispose();
|
scoreSubscription?.Dispose();
|
||||||
scoreSubscription = realmFactory.Context.All<ScoreInfo>()
|
scoreSubscription = realmFactory.RegisterForNotifications(realm =>
|
||||||
.Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
|
realm.All<ScoreInfo>()
|
||||||
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
|
.Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
|
||||||
+ $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2"
|
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
|
||||||
+ $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName)
|
+ $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2"
|
||||||
.OrderByDescending(s => s.TotalScore)
|
+ $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName)
|
||||||
.QueryAsyncWithNotifications((items, changes, ___) =>
|
.OrderByDescending(s => s.TotalScore),
|
||||||
{
|
(items, changes, ___) =>
|
||||||
Rank = items.FirstOrDefault()?.Rank;
|
{
|
||||||
// Required since presence is changed via IsPresent override
|
Rank = items.FirstOrDefault()?.Rank;
|
||||||
Invalidate(Invalidation.Presence);
|
// Required since presence is changed via IsPresent override
|
||||||
});
|
Invalidate(Invalidation.Presence);
|
||||||
|
});
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,9 +44,13 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
beatmapInfo = value;
|
beatmapInfo = value;
|
||||||
Scores = null;
|
Scores = null;
|
||||||
|
|
||||||
UpdateScores();
|
if (IsOnlineScope)
|
||||||
if (IsLoaded)
|
UpdateScores();
|
||||||
refreshRealmSubscription();
|
else
|
||||||
|
{
|
||||||
|
if (IsLoaded)
|
||||||
|
refreshRealmSubscription();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,15 +113,14 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
if (beatmapInfo == null)
|
if (beatmapInfo == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
scoreSubscription = realmFactory.Context.All<ScoreInfo>()
|
scoreSubscription = realmFactory.RegisterForNotifications(realm =>
|
||||||
.Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID)
|
realm.All<ScoreInfo>()
|
||||||
.QueryAsyncWithNotifications((_, changes, ___) =>
|
.Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} = $0", beatmapInfo.ID),
|
||||||
{
|
(_, changes, ___) =>
|
||||||
if (changes == null)
|
{
|
||||||
return;
|
if (!IsOnlineScope)
|
||||||
|
RefreshScores();
|
||||||
RefreshScores();
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Reset()
|
protected override void Reset()
|
||||||
|
@ -17,6 +17,7 @@ using osu.Game.Online.Spectator;
|
|||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Spectate
|
namespace osu.Game.Screens.Spectate
|
||||||
{
|
{
|
||||||
@ -79,23 +80,21 @@ namespace osu.Game.Screens.Spectate
|
|||||||
playingUserStates.BindTo(spectatorClient.PlayingUserStates);
|
playingUserStates.BindTo(spectatorClient.PlayingUserStates);
|
||||||
playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true);
|
playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true);
|
||||||
|
|
||||||
realmSubscription = realmContextFactory.Context
|
realmSubscription = realmContextFactory.RegisterForNotifications(
|
||||||
.All<BeatmapSetInfo>()
|
realm => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
|
||||||
.Where(s => !s.DeletePending)
|
|
||||||
.QueryAsyncWithNotifications((items, changes, ___) =>
|
|
||||||
{
|
|
||||||
if (changes?.InsertedIndices == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (int c in changes.InsertedIndices)
|
|
||||||
beatmapUpdated(items[c]);
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach ((int id, var _) in userMap)
|
foreach ((int id, var _) in userMap)
|
||||||
spectatorClient.WatchUser(id);
|
spectatorClient.WatchUser(id);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> items, ChangeSet changes, Exception ___)
|
||||||
|
{
|
||||||
|
if (changes?.InsertedIndices == null) return;
|
||||||
|
|
||||||
|
foreach (int c in changes.InsertedIndices) beatmapUpdated(items[c]);
|
||||||
|
}
|
||||||
|
|
||||||
private void beatmapUpdated(BeatmapSetInfo beatmapSet)
|
private void beatmapUpdated(BeatmapSetInfo beatmapSet)
|
||||||
{
|
{
|
||||||
foreach ((int userId, _) in userMap)
|
foreach ((int userId, _) in userMap)
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Development;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -72,8 +73,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
[TearDownSteps]
|
[TearDownSteps]
|
||||||
public void TearDownSteps()
|
public void TearDownSteps()
|
||||||
{
|
{
|
||||||
AddStep("exit game", () => Game.Exit());
|
if (DebugUtils.IsNUnitRunning)
|
||||||
AddUntilStep("wait for game exit", () => Game.Parent == null);
|
{
|
||||||
|
AddStep("exit game", () => Game.Exit());
|
||||||
|
AddUntilStep("wait for game exit", () => Game.Parent == null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void CreateGame()
|
protected void CreateGame()
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Development;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
@ -48,7 +49,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
public virtual void SetUpSteps() => addExitAllScreensStep();
|
public virtual void SetUpSteps() => addExitAllScreensStep();
|
||||||
|
|
||||||
[TearDownSteps]
|
[TearDownSteps]
|
||||||
public virtual void TearDownSteps() => addExitAllScreensStep();
|
public virtual void TearDownSteps()
|
||||||
|
{
|
||||||
|
if (DebugUtils.IsNUnitRunning)
|
||||||
|
addExitAllScreensStep();
|
||||||
|
}
|
||||||
|
|
||||||
private void addExitAllScreensStep()
|
private void addExitAllScreensStep()
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -23,6 +24,12 @@ namespace osu.Game.Users.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ShowPlaceholderOnNull = true;
|
public bool ShowPlaceholderOnNull = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform an action in addition to showing the country ranking.
|
||||||
|
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX).
|
||||||
|
/// </summary>
|
||||||
|
public Action Action;
|
||||||
|
|
||||||
public UpdateableFlag(Country country = null)
|
public UpdateableFlag(Country country = null)
|
||||||
{
|
{
|
||||||
Country = country;
|
Country = country;
|
||||||
@ -52,6 +59,7 @@ namespace osu.Game.Users.Drawables
|
|||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
{
|
{
|
||||||
|
Action?.Invoke();
|
||||||
rankingsOverlay?.ShowCountry(Country);
|
rankingsOverlay?.ShowCountry(Country);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,8 @@ namespace osu.Game.Users
|
|||||||
|
|
||||||
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
|
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
|
||||||
{
|
{
|
||||||
Size = new Vector2(39, 26)
|
Size = new Vector2(39, 26),
|
||||||
|
Action = Action,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon
|
protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon
|
||||||
|
Loading…
Reference in New Issue
Block a user