diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index cd3fb7eb61..be8159a7cc 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -96,6 +97,8 @@ namespace osu.Desktop
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
+ Debug.Assert(OperatingSystem.IsWindows());
+
return new SquirrelUpdateManager();
default:
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index b944068e78..e317a44bc3 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
+using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.LegacyIpc;
@@ -12,6 +13,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IPC;
using osu.Game.Tournament;
+using Squirrel;
namespace osu.Desktop
{
@@ -24,6 +26,10 @@ namespace osu.Desktop
[STAThread]
public static void Main(string[] args)
{
+ // run Squirrel first, as the app may exit after these run
+ if (OperatingSystem.IsWindows())
+ setupSquirrel();
+
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
@@ -104,6 +110,23 @@ namespace osu.Desktop
}
}
+ [SupportedOSPlatform("windows")]
+ private static void setupSquirrel()
+ {
+ SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) =>
+ {
+ tools.CreateShortcutForThisExe();
+ tools.CreateUninstallerRegistryEntry();
+ }, onAppUninstall: (version, tools) =>
+ {
+ tools.RemoveShortcutForThisExe();
+ tools.RemoveUninstallerRegistryEntry();
+ }, onEveryRun: (version, tools, firstRun) =>
+ {
+ tools.SetProcessAppUserModelId();
+ });
+ }
+
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
///
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 7b60bc03e4..b307146b10 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Runtime.Versioning;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -16,10 +17,11 @@ using osu.Game.Overlays.Notifications;
using osuTK;
using osuTK.Graphics;
using Squirrel;
-using LogLevel = Splat.LogLevel;
+using Squirrel.SimpleSplat;
namespace osu.Desktop.Updater
{
+ [SupportedOSPlatform("windows")]
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{
private UpdateManager updateManager;
@@ -34,12 +36,14 @@ namespace osu.Desktop.Updater
///
private bool updatePending;
+ private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
+
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification)
{
notificationOverlay = notification;
- Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
+ SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
}
protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
@@ -49,9 +53,11 @@ namespace osu.Desktop.Updater
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
+ const string github_token = null; // TODO: populate.
+
try
{
- updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false);
+ updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
@@ -201,11 +207,11 @@ namespace osu.Desktop.Updater
}
}
- private class SquirrelLogger : Splat.ILogger, IDisposable
+ private class SquirrelLogger : ILogger, IDisposable
{
- public LogLevel Level { get; set; } = LogLevel.Info;
+ public Squirrel.SimpleSplat.LogLevel Level { get; set; } = Squirrel.SimpleSplat.LogLevel.Info;
- public void Write(string message, LogLevel logLevel)
+ public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel)
{
if (logLevel < Level)
return;
diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest
index 2e9127bf44..a11cee132c 100644
--- a/osu.Desktop/app.manifest
+++ b/osu.Desktop/app.manifest
@@ -1,6 +1,7 @@
+ 1
@@ -17,4 +18,4 @@
true
-
\ No newline at end of file
+
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index b1117bf796..32ead231c7 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,10 +24,10 @@
+
-
all
diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
index 02d617d0e0..d99bcc092d 100644
--- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
+++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs
@@ -1,25 +1,80 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
+using osu.Game.Database;
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 TestSubscriptionCollectionAndPropertyChanges()
+ {
+ int collectionChanges = 0;
+ int propertyChanges = 0;
+
+ ChangeSet? lastChanges = null;
+
+ RunTestWithRealm((realm, _) =>
+ {
+ var registration = realm.RegisterForNotifications(r => r.All(), onChanged);
+
+ realm.Run(r => r.Refresh());
+
+ realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+ realm.Run(r => r.Refresh());
+
+ Assert.That(collectionChanges, Is.EqualTo(1));
+ Assert.That(propertyChanges, Is.EqualTo(0));
+ Assert.That(lastChanges?.InsertedIndices, Has.One.Items);
+ Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
+ Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
+
+ realm.Write(r => r.All().First().Beatmaps.First().CountdownOffset = 5);
+ realm.Run(r => r.Refresh());
+
+ Assert.That(collectionChanges, Is.EqualTo(1));
+ Assert.That(propertyChanges, Is.EqualTo(1));
+ Assert.That(lastChanges?.InsertedIndices, Is.Empty);
+ Assert.That(lastChanges?.ModifiedIndices, Has.One.Items);
+ Assert.That(lastChanges?.NewModifiedIndices, Has.One.Items);
+
+ registration.Dispose();
+ });
+
+ void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error)
+ {
+ lastChanges = changes;
+
+ if (changes == null)
+ return;
+
+ if (changes.HasCollectionChanges())
+ {
+ Interlocked.Increment(ref collectionChanges);
+ }
+ else
+ {
+ Interlocked.Increment(ref propertyChanges);
+ }
+ }
+ }
+
[Test]
public void TestSubscriptionWithAsyncWrite()
{
@@ -47,6 +102,28 @@ namespace osu.Game.Tests.Database
void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => lastChanges = changes;
}
+ [Test]
+ public void TestPropertyChangedSubscription()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ bool? receivedValue = null;
+
+ realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+
+ using (realm.SubscribeToPropertyChanged(r => r.All().First(), setInfo => setInfo.Protected, val => receivedValue = val))
+ {
+ Assert.That(receivedValue, Is.False);
+
+ realm.Write(r => r.All().First().Protected = true);
+
+ realm.Run(r => r.Refresh());
+
+ Assert.That(receivedValue, Is.True);
+ }
+ });
+ }
+
[Test]
public void TestSubscriptionWithContextLoss()
{
@@ -163,5 +240,41 @@ namespace osu.Game.Tests.Database
Assert.That(beatmapSetInfo, Is.Null);
});
}
+
+ [Test]
+ public void TestPropertyChangedSubscriptionWithContextLoss()
+ {
+ RunTestWithRealm((realm, _) =>
+ {
+ bool? receivedValue = null;
+
+ realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
+
+ var subscription = realm.SubscribeToPropertyChanged(
+ r => r.All().First(),
+ setInfo => setInfo.Protected,
+ val => receivedValue = val);
+
+ Assert.That(receivedValue, Is.Not.Null);
+ receivedValue = null;
+
+ using (realm.BlockAllOperations())
+ {
+ }
+
+ // re-registration after context restore.
+ realm.Run(r => r.Refresh());
+ Assert.That(receivedValue, Is.Not.Null);
+
+ subscription.Dispose();
+ receivedValue = null;
+
+ using (realm.BlockAllOperations())
+ Assert.That(receivedValue, Is.Null);
+
+ realm.Run(r => r.Refresh());
+ Assert.That(receivedValue, Is.Null);
+ });
+ }
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 88862ea28b..6457a23a1b 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -1,29 +1,23 @@
// Copyright (c) ppy Pty Ltd . 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.IO;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
-using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
-using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
@@ -118,59 +112,6 @@ namespace osu.Game.Tests.Gameplay
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
}
- [TestCase(typeof(OsuModDoubleTime), 1.5)]
- [TestCase(typeof(OsuModHalfTime), 0.75)]
- [TestCase(typeof(ModWindUp), 1.5)]
- [TestCase(typeof(ModWindDown), 0.75)]
- [TestCase(typeof(OsuModDoubleTime), 2)]
- [TestCase(typeof(OsuModHalfTime), 0.5)]
- [TestCase(typeof(ModWindUp), 2)]
- [TestCase(typeof(ModWindDown), 0.5)]
- public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
- {
- GameplayClockContainer gameplayContainer = null;
- StoryboardSampleInfo sampleInfo = null;
- TestDrawableStoryboardSample sample = null;
-
- Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
-
- switch (testedMod)
- {
- case ModRateAdjust m:
- m.SpeedChange.Value = expectedRate;
- break;
-
- case ModTimeRamp m:
- m.FinalRate.Value = m.InitialRate.Value = expectedRate;
- break;
- }
-
- AddStep("setup storyboard sample", () =>
- {
- Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this);
- SelectedMods.Value = new[] { testedMod };
-
- var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
-
- Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
- {
- Child = beatmapSkinSourceContainer
- });
-
- beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1))
- {
- Clock = gameplayContainer.GameplayClock
- });
- });
-
- AddStep("start", () => gameplayContainer.Start());
-
- AddAssert("sample playback rate matches mod rates", () =>
- testedMod != null && Precision.AlmostEquals(
- sample.ChildrenOfType().First().AggregateFrequency.Value,
- ((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime)));
- }
-
[Test]
public void TestSamplePlaybackWithBeatmapHitsoundsOff()
{
diff --git a/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs b/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs
new file mode 100644
index 0000000000..662660bce4
--- /dev/null
+++ b/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+using NUnit.Framework;
+using osu.Game.IO.Serialization;
+using osu.Game.Online.Solo;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Online
+{
+ ///
+ /// Basic testing to ensure our attribute-based naming is correctly working.
+ ///
+ [TestFixture]
+ public class TestSubmittableScoreJsonSerialization
+ {
+ [Test]
+ public void TestScoreSerialisationViaExtensionMethod()
+ {
+ var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
+
+ string serialised = score.Serialize();
+
+ Assert.That(serialised, Contains.Substring("large_tick_hit"));
+ Assert.That(serialised, Contains.Substring("\"rank\": \"S\""));
+ }
+
+ [Test]
+ public void TestScoreSerialisationWithoutSettings()
+ {
+ var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
+
+ string serialised = JsonConvert.SerializeObject(score);
+
+ Assert.That(serialised, Contains.Substring("large_tick_hit"));
+ Assert.That(serialised, Contains.Substring("\"rank\":\"S\""));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs
index 3b6d02c67c..014ccb1652 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs
@@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
storyboardContainer.Clock = decoupledClock;
- storyboard = working.Storyboard.CreateDrawable(Beatmap.Value);
+ storyboard = working.Storyboard.CreateDrawable(SelectedMods.Value);
storyboard.Passing = false;
storyboardContainer.Add(storyboard);
@@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay
sb = decoder.Decode(bfr);
}
- storyboard = sb.CreateDrawable(Beatmap.Value);
+ storyboard = sb.CreateDrawable(SelectedMods.Value);
storyboardContainer.Add(storyboard);
decoupledClock.ChangeSource(Beatmap.Value.Track);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
index 95603b5c04..909cab5e3d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs
@@ -1,17 +1,23 @@
// Copyright (c) ppy Pty Ltd . 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.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics.Audio;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -19,6 +25,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private Storyboard storyboard;
+ private IReadOnlyList storyboardMods;
+
+ protected override bool HasCustomSteps => true;
+
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
@@ -31,42 +41,107 @@ namespace osu.Game.Tests.Visual.Gameplay
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20));
}
+ [SetUp]
+ public void SetUp() => storyboardMods = Array.Empty();
+
[Test]
public void TestStoryboardSamplesStopDuringPause()
{
- checkForFirstSamplePlayback();
+ createPlayerTest();
AddStep("player paused", () => Player.Pause());
AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value);
- AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
+ allStoryboardSamplesStopped();
AddStep("player resume", () => Player.Resume());
- AddUntilStep("any storyboard samples playing after resume", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
+ waitUntilStoryboardSamplesPlay();
}
[Test]
public void TestStoryboardSamplesStopOnSkip()
{
- checkForFirstSamplePlayback();
+ createPlayerTest();
- AddStep("skip intro", () => InputManager.Key(osuTK.Input.Key.Space));
- AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
+ skipIntro();
+ allStoryboardSamplesStopped();
- AddUntilStep("any storyboard samples playing after skip", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
+ waitUntilStoryboardSamplesPlay();
}
- private void checkForFirstSamplePlayback()
+ [TestCase(typeof(OsuModDoubleTime), 1.5)]
+ [TestCase(typeof(OsuModDoubleTime), 2)]
+ [TestCase(typeof(OsuModHalfTime), 0.75)]
+ [TestCase(typeof(OsuModHalfTime), 0.5)]
+ public void TestStoryboardSamplesPlaybackWithRateAdjustMods(Type expectedMod, double expectedRate)
{
- AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null);
- AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
+ AddStep("setup mod", () =>
+ {
+ ModRateAdjust testedMod = (ModRateAdjust)Activator.CreateInstance(expectedMod).AsNonNull();
+ testedMod.SpeedChange.Value = expectedRate;
+ storyboardMods = new[] { testedMod };
+ });
+
+ createPlayerTest();
+ skipIntro();
+
+ AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound =>
+ sound.ChildrenOfType().First().AggregateFrequency.Value == expectedRate));
}
+ [TestCase(typeof(ModWindUp), 0.5, 2)]
+ [TestCase(typeof(ModWindUp), 1.51, 2)]
+ [TestCase(typeof(ModWindDown), 2, 0.5)]
+ [TestCase(typeof(ModWindDown), 0.99, 0.5)]
+ public void TestStoryboardSamplesPlaybackWithTimeRampMods(Type expectedMod, double initialRate, double finalRate)
+ {
+ AddStep("setup mod", () =>
+ {
+ ModTimeRamp testedMod = (ModTimeRamp)Activator.CreateInstance(expectedMod).AsNonNull();
+ testedMod.InitialRate.Value = initialRate;
+ testedMod.FinalRate.Value = finalRate;
+ storyboardMods = new[] { testedMod };
+ });
+
+ createPlayerTest();
+ skipIntro();
+
+ ModTimeRamp gameplayMod = null;
+
+ AddUntilStep("mod speed change updated", () =>
+ {
+ gameplayMod = Player.GameplayState.Mods.OfType().Single();
+ return gameplayMod.SpeedChange.Value != initialRate;
+ });
+
+ AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound =>
+ sound.ChildrenOfType().First().AggregateFrequency.Value == gameplayMod.SpeedChange.Value));
+ }
+
+ private void createPlayerTest()
+ {
+ CreateTest(null);
+
+ AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null);
+ waitUntilStoryboardSamplesPlay();
+ }
+
+ private void waitUntilStoryboardSamplesPlay() => AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
+
+ private void allStoryboardSamplesStopped() => AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
+
+ private void skipIntro() => AddStep("skip intro", () => InputManager.Key(Key.Space));
+
private IEnumerable allStoryboardSamples => Player.ChildrenOfType();
protected override bool AllowFail => false;
+ protected override TestPlayer CreatePlayer(Ruleset ruleset)
+ {
+ SelectedMods.Value = SelectedMods.Value.Concat(storyboardMods).ToArray();
+ return new TestPlayer(true, false);
+ }
+
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
- protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard ?? this.storyboard, Clock, Audio);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs
new file mode 100644
index 0000000000..22a8fa8a46
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . 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.Allocation;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestScenePopupScreenTitle : OsuTestScene
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
+ [Test]
+ public void TestPopupScreenTitle()
+ {
+ AddStep("create content", () =>
+ {
+ Child = new PopupScreenTitle
+ {
+ Title = "Popup Screen Title",
+ Description = string.Join(" ", Enumerable.Repeat("This is a description.", 20)),
+ Close = () => { }
+ };
+ });
+ }
+
+ [Test]
+ public void TestDisabledExit()
+ {
+ AddStep("create content", () =>
+ {
+ Child = new PopupScreenTitle
+ {
+ Title = "Popup Screen Title",
+ Description = "This is a description."
+ };
+ });
+ }
+ }
+}
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index fb3052d850..af7c485c57 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -3,9 +3,11 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -318,6 +320,66 @@ namespace osu.Game.Database
}
}
+ ///
+ /// Subscribe to the property of a realm object to watch for changes.
+ ///
+ ///
+ /// On subscribing, unless the does not match an object, an initial invocation of will occur immediately.
+ /// Further invocations will occur when the value changes, but may also fire on a realm recycle with no actual value change.
+ ///
+ /// A function to retrieve the relevant model from realm.
+ /// A function to traverse to the relevant property from the model.
+ /// A function to be invoked when a change of value occurs.
+ /// The type of the model.
+ /// The type of the property to be watched.
+ ///
+ /// A subscription token. It must be kept alive for as long as you want to receive change notifications.
+ /// To stop receiving notifications, call .
+ ///
+ public IDisposable SubscribeToPropertyChanged(Func modelAccessor, Expression> propertyLookup, Action onChanged)
+ where TModel : RealmObjectBase
+ {
+ return RegisterCustomSubscription(r =>
+ {
+ string propertyName = getMemberName(propertyLookup);
+
+ var model = Run(modelAccessor);
+ var propLookupCompiled = propertyLookup.Compile();
+
+ if (model == null)
+ return null;
+
+ model.PropertyChanged += onPropertyChanged;
+
+ // Update initial value immediately.
+ onChanged(propLookupCompiled(model));
+
+ return new InvokeOnDisposal(() => model.PropertyChanged -= onPropertyChanged);
+
+ void onPropertyChanged(object sender, PropertyChangedEventArgs args)
+ {
+ if (args.PropertyName == propertyName)
+ onChanged(propLookupCompiled(model));
+ }
+ });
+
+ static string getMemberName(Expression> expression)
+ {
+ if (!(expression is LambdaExpression lambda))
+ throw new ArgumentException("Outermost expression must be a lambda expression", nameof(expression));
+
+ if (!(lambda.Body is MemberExpression memberExpression))
+ throw new ArgumentException("Lambda body must be a member access expression", nameof(expression));
+
+ // TODO: nested access can be supported, with more iteration here
+ // (need to iteratively soft-cast `memberExpression.Expression` into `MemberExpression`s until `lambda.Parameters[0]` is hit)
+ if (memberExpression.Expression != lambda.Parameters[0])
+ throw new ArgumentException("Nested access expressions are not supported", nameof(expression));
+
+ return memberExpression.Member.Name;
+ }
+ }
+
///
/// Run work on realm that will be run every time the update thread realm instance gets recycled.
///
diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs
index e6f3dba39f..551b84f7b6 100644
--- a/osu.Game/Database/RealmExtensions.cs
+++ b/osu.Game/Database/RealmExtensions.cs
@@ -4,6 +4,8 @@
using System;
using Realms;
+#nullable enable
+
namespace osu.Game.Database
{
public static class RealmExtensions
@@ -22,5 +24,14 @@ namespace osu.Game.Database
transaction.Commit();
return result;
}
+
+ ///
+ /// Whether the provided change set has changes to the top level collection.
+ ///
+ ///
+ /// Realm subscriptions fire on both collection and property changes (including *all* nested properties).
+ /// Quite often we only care about changes at a collection level. This can be used to guard and early-return when no such changes are in a callback.
+ ///
+ public static bool HasCollectionChanges(this ChangeSet changes) => changes.InsertedIndices.Length > 0 || changes.DeletedIndices.Length > 0 || changes.Moves.Length > 0;
}
}
diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs
index 56ef87c1f4..7aed442800 100644
--- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs
+++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs
@@ -3,12 +3,15 @@
#nullable enable
+using System.Collections.Generic;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
+using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Graphics.Backgrounds
@@ -20,6 +23,9 @@ namespace osu.Game.Graphics.Backgrounds
[Resolved(CanBeNull = true)]
private MusicController? musicController { get; set; }
+ [Resolved]
+ private IBindable> mods { get; set; } = null!;
+
public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1")
: base(beatmap, fallbackTextureName)
{
@@ -39,7 +45,7 @@ namespace osu.Game.Graphics.Backgrounds
{
RelativeSizeAxes = Axes.Both,
Volume = { Value = 0 },
- Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = storyboardClock }
+ Child = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock }
}, AddInternal);
}
diff --git a/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs b/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs
new file mode 100644
index 0000000000..5b7db09e77
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs
@@ -0,0 +1,154 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable enable
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ public class PopupScreenTitle : CompositeDrawable
+ {
+ public LocalisableString Title
+ {
+ set => titleSpriteText.Text = value;
+ }
+
+ public LocalisableString Description
+ {
+ set => descriptionText.Text = value;
+ }
+
+ public Action? Close
+ {
+ get => closeButton.Action;
+ set => closeButton.Action = value;
+ }
+
+ private const float corner_radius = 14;
+ private const float main_area_height = 70;
+
+ private readonly Container underlayContainer;
+ private readonly Box underlayBackground;
+ private readonly Container contentContainer;
+ private readonly Box contentBackground;
+ private readonly OsuSpriteText titleSpriteText;
+ private readonly OsuTextFlowContainer descriptionText;
+ private readonly IconButton closeButton;
+
+ public PopupScreenTitle()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ InternalChild = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding
+ {
+ Horizontal = 70,
+ Top = -corner_radius
+ },
+ Children = new Drawable[]
+ {
+ underlayContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = main_area_height + 2 * corner_radius,
+ CornerRadius = corner_radius,
+ Masking = true,
+ BorderThickness = 2,
+ Child = underlayBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ contentContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = main_area_height + corner_radius,
+ CornerRadius = corner_radius,
+ Masking = true,
+ BorderThickness = 2,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Colour4.Black.Opacity(0.1f),
+ Offset = new Vector2(0, 1),
+ Radius = 3
+ },
+ Children = new Drawable[]
+ {
+ contentBackground = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Margin = new MarginPadding { Top = corner_radius },
+ Padding = new MarginPadding { Horizontal = 100 },
+ Children = new Drawable[]
+ {
+ titleSpriteText = new OsuSpriteText
+ {
+ Font = OsuFont.TorusAlternate.With(size: 20)
+ },
+ descriptionText = new OsuTextFlowContainer(t =>
+ {
+ t.Font = OsuFont.Default.With(size: 12);
+ })
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }
+ }
+ },
+ closeButton = new IconButton
+ {
+ Icon = FontAwesome.Solid.Times,
+ Scale = new Vector2(0.6f),
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Margin = new MarginPadding
+ {
+ Right = 21,
+ Top = corner_radius
+ }
+ }
+ }
+ }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ underlayContainer.BorderColour = ColourInfo.GradientVertical(Colour4.Black, colourProvider.Dark4);
+ underlayBackground.Colour = colourProvider.Dark4;
+
+ contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Dark3, colourProvider.Dark1);
+ contentBackground.Colour = colourProvider.Dark3;
+
+ closeButton.IconHoverColour = colourProvider.Highlight1;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
index 4da8d6a554..fd64cc2056 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
set => Component.Text = value;
}
- public Container TabbableContentContainer
+ public CompositeDrawable TabbableContentContainer
{
set => Component.TabbableContentContainer = value;
}
diff --git a/osu.Game/Online/Solo/SubmittableScore.cs b/osu.Game/Online/Solo/SubmittableScore.cs
index 4e4dae5157..9b6da1844a 100644
--- a/osu.Game/Online/Solo/SubmittableScore.cs
+++ b/osu.Game/Online/Solo/SubmittableScore.cs
@@ -46,9 +46,6 @@ namespace osu.Game.Online.Solo
[JsonProperty("mods")]
public APIMod[] Mods { get; set; }
- [JsonProperty("user")]
- public APIUser User { get; set; }
-
[JsonProperty("statistics")]
public Dictionary Statistics { get; set; }
@@ -67,7 +64,6 @@ namespace osu.Game.Online.Solo
RulesetID = score.RulesetID;
Passed = score.Passed;
Mods = score.APIMods;
- User = score.User;
Statistics = score.Statistics;
}
}
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index a254f9b760..514232db69 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -5,6 +5,7 @@ using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
+using System.Runtime.Serialization;
using osu.Framework.Utils;
namespace osu.Game.Rulesets.Scoring
@@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates that the object has not been judged yet.
///
[Description(@"")]
+ [EnumMember(Value = "none")]
[Order(14)]
None,
@@ -27,32 +29,39 @@ namespace osu.Game.Rulesets.Scoring
/// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time).
///
[Description(@"Miss")]
+ [EnumMember(Value = "miss")]
[Order(5)]
Miss,
[Description(@"Meh")]
+ [EnumMember(Value = "meh")]
[Order(4)]
Meh,
[Description(@"OK")]
+ [EnumMember(Value = "ok")]
[Order(3)]
Ok,
[Description(@"Good")]
+ [EnumMember(Value = "good")]
[Order(2)]
Good,
[Description(@"Great")]
+ [EnumMember(Value = "great")]
[Order(1)]
Great,
[Description(@"Perfect")]
+ [EnumMember(Value = "perfect")]
[Order(0)]
Perfect,
///
/// Indicates small tick miss.
///
+ [EnumMember(Value = "small_tick_miss")]
[Order(11)]
SmallTickMiss,
@@ -60,12 +69,14 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a small tick hit.
///
[Description(@"S Tick")]
+ [EnumMember(Value = "small_tick_hit")]
[Order(7)]
SmallTickHit,
///
/// Indicates a large tick miss.
///
+ [EnumMember(Value = "large_tick_miss")]
[Order(10)]
LargeTickMiss,
@@ -73,6 +84,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a large tick hit.
///
[Description(@"L Tick")]
+ [EnumMember(Value = "large_tick_hit")]
[Order(6)]
LargeTickHit,
@@ -80,6 +92,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a small bonus.
///
[Description("S Bonus")]
+ [EnumMember(Value = "small_bonus")]
[Order(9)]
SmallBonus,
@@ -87,18 +100,21 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a large bonus.
///
[Description("L Bonus")]
+ [EnumMember(Value = "large_bonus")]
[Order(8)]
LargeBonus,
///
/// Indicates a miss that should be ignored for scoring purposes.
///
+ [EnumMember(Value = "ignore_miss")]
[Order(13)]
IgnoreMiss,
///
/// Indicates a hit that should be ignored for scoring purposes.
///
+ [EnumMember(Value = "ignore_hit")]
[Order(12)]
IgnoreHit,
}
@@ -133,6 +149,30 @@ namespace osu.Game.Rulesets.Scoring
public static bool AffectsAccuracy(this HitResult result)
=> IsScorable(result) && !IsBonus(result);
+ ///
+ /// Whether a is a non-tick and non-bonus result.
+ ///
+ public static bool IsBasic(this HitResult result)
+ => IsScorable(result) && !IsTick(result) && !IsBonus(result);
+
+ ///
+ /// Whether a should be counted as a tick.
+ ///
+ public static bool IsTick(this HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ case HitResult.LargeTickMiss:
+ case HitResult.SmallTickHit:
+ case HitResult.SmallTickMiss:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
///
/// Whether a should be counted as bonus score.
///
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index 7d52645aa1..fc0952d4f0 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -75,9 +75,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader]
private void load()
{
+ FillFlowContainer flow;
+
Children = new Drawable[]
{
- new FillFlowContainer
+ flow = new FillFlowContainer
{
Width = 200,
Direction = FillDirection.Vertical,
@@ -94,6 +96,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
};
+ bank.TabbableContentContainer = flow;
+ volume.TabbableContentContainer = flow;
+
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs
index e25d83cfb0..0cf2cf6c54 100644
--- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs
+++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs
@@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Timing
set => slider.KeyboardStep = value;
}
+ public CompositeDrawable TabbableContentContainer
+ {
+ set => textBox.TabbableContentContainer = value;
+ }
+
private readonly BindableWithCurrent current = new BindableWithCurrent();
public Bindable Current
diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs
index f8cedddfbe..5a3ef1e9d3 100644
--- a/osu.Game/Screens/Play/DimmableStoryboard.cs
+++ b/osu.Game/Screens/Play/DimmableStoryboard.cs
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables;
@@ -18,6 +20,8 @@ namespace osu.Game.Screens.Play
public Container OverlayLayerContainer { get; private set; }
private readonly Storyboard storyboard;
+ private readonly IReadOnlyList mods;
+
private DrawableStoryboard drawableStoryboard;
///
@@ -28,9 +32,10 @@ namespace osu.Game.Screens.Play
///
public IBindable HasStoryboardEnded = new BindableBool(true);
- public DimmableStoryboard(Storyboard storyboard)
+ public DimmableStoryboard(Storyboard storyboard, IReadOnlyList mods)
{
this.storyboard = storyboard;
+ this.mods = mods;
}
[BackgroundDependencyLoader]
@@ -57,7 +62,7 @@ namespace osu.Game.Screens.Play
if (!ShowStoryboard.Value && !IgnoreUserSettings.Value)
return;
- drawableStoryboard = storyboard.CreateDrawable();
+ drawableStoryboard = storyboard.CreateDrawable(mods);
HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded);
if (async)
diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
index 2b6db5f59e..af58e9d910 100644
--- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
@@ -86,26 +85,10 @@ namespace osu.Game.Screens.Play
userAudioOffset = config.GetBindable(OsuSetting.AudioOffset);
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
- beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
- {
- var userSettings = r.Find(beatmap.BeatmapInfo.ID)?.UserSettings;
-
- if (userSettings == null) // only the case for tests.
- return null;
-
- void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
- {
- if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
- updateOffset();
- }
-
- updateOffset();
- userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
-
- return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
-
- void updateOffset() => userBeatmapOffsetClock.Offset = userSettings.Offset;
- });
+ beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
+ r => r.Find(beatmap.BeatmapInfo.ID)?.UserSettings,
+ settings => settings.Offset,
+ val => userBeatmapOffsetClock.Offset = val);
// sane default provided by ruleset.
startOffset = gameplayStartTime;
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 86ea412488..b6f576ff2b 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -359,7 +359,7 @@ namespace osu.Game.Screens.Play
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
private Drawable createUnderlayComponents() =>
- DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both };
+ DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both };
private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay)
{
diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
index d7125bd2db..c00b2f56dc 100644
--- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
@@ -120,24 +119,10 @@ namespace osu.Game.Screens.Play.PlayerSettings
ReferenceScore.BindValueChanged(scoreChanged, true);
- beatmapOffsetSubscription = realm.RegisterCustomSubscription(r =>
- {
- var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings;
-
- if (userSettings == null) // only the case for tests.
- return null;
-
- Current.Value = userSettings.Offset;
- userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
-
- return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
-
- void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
- {
- if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
- Current.Value = userSettings.Offset;
- }
- });
+ beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
+ r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
+ settings => settings.Offset,
+ val => Current.Value = val);
Current.BindValueChanged(currentChanged);
}
diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
index 760915b528..a000cfd5fc 100644
--- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
+++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs
@@ -16,6 +16,9 @@ namespace osu.Game.Screens.Select.Carousel
{
public class SetPanelContent : CompositeDrawable
{
+ // Disallow interacting with difficulty icons on a panel until the panel has been selected.
+ public override bool PropagatePositionalInputSubTree => carouselSet.State.Value == CarouselItemState.Selected;
+
private readonly CarouselBeatmapSet carouselSet;
public SetPanelContent(CarouselBeatmapSet carouselSet)
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index eb0addd377..8d1654eb1d 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -191,6 +191,11 @@ namespace osu.Game.Screens.Select.Leaderboards
if (cancellationToken.IsCancellationRequested)
return;
+ // This subscription may fire from changes to linked beatmaps, which we don't care about.
+ // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
+ if (changes?.HasCollectionChanges() == false)
+ return;
+
var scores = sender.AsEnumerable();
if (filterMods && !mods.Value.Any())
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
index e6528a83bd..a0fb7b0b4a 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . 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 System.Threading;
using osuTK;
@@ -11,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Platform;
using osu.Game.Database;
+using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play;
using osu.Game.Stores;
@@ -50,14 +53,18 @@ namespace osu.Game.Storyboards.Drawables
private double? lastEventEndTime;
+ [Cached(typeof(IReadOnlyList))]
+ public IReadOnlyList Mods { get; }
+
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- public DrawableStoryboard(Storyboard storyboard)
+ public DrawableStoryboard(Storyboard storyboard, IReadOnlyList mods = null)
{
Storyboard = storyboard;
+ Mods = mods ?? Array.Empty();
Size = new Vector2(640, 480);
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs
index 672274a2ad..4e3f72512c 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs
@@ -28,17 +28,20 @@ namespace osu.Game.Storyboards.Drawables
LifetimeStart = sampleInfo.StartTime;
}
- [Resolved]
- private IBindable> mods { get; set; }
+ [Resolved(CanBeNull = true)]
+ private IReadOnlyList mods { get; set; }
protected override void SkinChanged(ISkinSource skin)
{
base.SkinChanged(skin);
- foreach (var mod in mods.Value.OfType())
+ if (mods != null)
{
- foreach (var sample in DrawableSamples)
- mod.ApplyToSample(sample);
+ foreach (var mod in mods.OfType())
+ {
+ foreach (var sample in DrawableSamples)
+ mod.ApplyToSample(sample);
+ }
}
}
diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs
index c4864c0334..2faed98ae0 100644
--- a/osu.Game/Storyboards/Storyboard.cs
+++ b/osu.Game/Storyboards/Storyboard.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
+using osu.Game.Rulesets.Mods;
using osu.Game.Skinning;
using osu.Game.Storyboards.Drawables;
@@ -90,8 +91,8 @@ namespace osu.Game.Storyboards
}
}
- public DrawableStoryboard CreateDrawable(IWorkingBeatmap working = null) =>
- new DrawableStoryboard(this);
+ public DrawableStoryboard CreateDrawable(IReadOnlyList mods = null) =>
+ new DrawableStoryboard(this, mods);
public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore)
{
diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs
index 368f792e28..d463905cf4 100644
--- a/osu.Game/Tests/Visual/TestPlayer.cs
+++ b/osu.Game/Tests/Visual/TestPlayer.cs
@@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
- ///
- /// Mods from *player* (not OsuScreen).
- ///
public new Bindable> Mods => base.Mods;
public new HUDOverlay HUDOverlay => base.HUDOverlay;