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;