diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index a69070e93e..af8206d95a 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.UI; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; @@ -99,6 +100,11 @@ namespace osu.Game.Rulesets.Catch new MultiMod(new CatchModAutoplay(), new ModCinema()), new CatchModRelax(), }; + case ModType.Fun: + return new Mod[] + { + new MultiMod(new ModWindUp<CatchHitObject>(), new ModWindDown<CatchHitObject>()) + }; default: return new Mod[] { }; } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs index 612df5bde5..692e63fa69 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModAutoplay : ModAutoplay<CatchHitObject> { - protected override Score CreateReplayScore(Beatmap<CatchHitObject> beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, Replay = new CatchAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 2fd05483ef..daa3f61de3 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -6,17 +6,20 @@ using System.Linq; using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Replays; +using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Catch.Replays { - internal class CatchAutoGenerator : AutoGenerator<CatchHitObject> + internal class CatchAutoGenerator : AutoGenerator { public const double RELEASE_DELAY = 20; - public CatchAutoGenerator(Beatmap<CatchHitObject> beatmap) + public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap; + + public CatchAutoGenerator(IBeatmap beatmap) : base(beatmap) { Replay = new Replay(); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b40093844b..2b6b7377ae 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -12,6 +12,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; @@ -145,6 +146,11 @@ namespace osu.Game.Rulesets.Mania { new MultiMod(new ManiaModAutoplay(), new ModCinema()), }; + case ModType.Fun: + return new Mod[] + { + new MultiMod(new ModWindUp<ManiaHitObject>(), new ModWindDown<ManiaHitObject>()) + }; default: return new Mod[] { }; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index 02eb7ac883..c05e979e9a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModAutoplay : ModAutoplay<ManiaHitObject> { - protected override Score CreateReplayScore(Beatmap<ManiaHitObject> beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index df6afa040e..65b7d54cd2 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -5,13 +5,12 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Replays; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Mania.Replays { - internal class ManiaAutoGenerator : AutoGenerator<ManiaHitObject> + internal class ManiaAutoGenerator : AutoGenerator { public const double RELEASE_DELAY = 20; diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs index 3d553c334f..5c1e775c01 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs @@ -16,18 +16,18 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestCaseGameplayCursor : OsuTestCase, IProvideCursor { - private GameplayCursor cursor; + private GameplayCursorContainer cursorContainer; public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(CursorTrail) }; - public CursorContainer Cursor => cursor; + public CursorContainer Cursor => cursorContainer; public bool ProvidingUserCursor => true; [BackgroundDependencyLoader] private void load() { - Add(cursor = new GameplayCursor { RelativeSizeAxes = Axes.Both }); + Add(cursorContainer = new GameplayCursorContainer { RelativeSizeAxes = Axes.Both }); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs index c293a5a4f8..7886a2393c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; @@ -16,8 +15,14 @@ namespace osu.Game.Rulesets.Osu.Edit { } - protected override CursorContainer CreateCursor() => null; + protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor { Size = Vector2.One }; - protected override Playfield CreatePlayfield() => new OsuPlayfield { Size = Vector2.One }; + private class OsuPlayfieldNoCursor : OsuPlayfield + { + public OsuPlayfieldNoCursor() + { + Cursor?.Expire(); + } + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index d7ba0d4da9..bea2bbcb32 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); - protected override Score CreateReplayScore(Beatmap<OsuHitObject> beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, Replay = new OsuAutoGenerator(beatmap).Generate() diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 20752517d5..d9c046a579 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -13,6 +13,7 @@ using osu.Game.Overlays.Settings; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; @@ -128,7 +129,8 @@ namespace osu.Game.Rulesets.Osu { new OsuModTransform(), new OsuModWiggle(), - new OsuModGrow() + new OsuModGrow(), + new MultiMod(new ModWindUp<OsuHitObject>(), new ModWindDown<OsuHitObject>()), }; default: return new Mod[] { }; diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index b0fb85d7ed..c1aaa7767e 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -10,12 +10,15 @@ using System.Linq; using osu.Framework.Graphics; using osu.Game.Replays; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Replays { public class OsuAutoGenerator : OsuAutoGeneratorBase { + public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap; + #region Parameters /// <summary> @@ -42,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Replays #region Construction / Initialisation - public OsuAutoGenerator(Beatmap<OsuHitObject> beatmap) + public OsuAutoGenerator(IBeatmap beatmap) : base(beatmap) { // Already superhuman, but still somewhat realistic diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index 9a60f0cafc..9ab358ee12 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -3,7 +3,6 @@ using osuTK; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Osu.Objects; using System; using System.Collections.Generic; using osu.Game.Replays; @@ -12,7 +11,7 @@ using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Osu.Replays { - public abstract class OsuAutoGeneratorBase : AutoGenerator<OsuHitObject> + public abstract class OsuAutoGeneratorBase : AutoGenerator { #region Constants @@ -35,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Replays protected Replay Replay; protected List<ReplayFrame> Frames => Replay.Frames; - protected OsuAutoGeneratorBase(Beatmap<OsuHitObject> beatmap) + protected OsuAutoGeneratorBase(IBeatmap beatmap) : base(beatmap) { Replay = new Replay(); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs similarity index 97% rename from osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs rename to osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs index ef126cdf7d..8c6723f5be 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.Cursor { - public class GameplayCursor : CursorContainer, IKeyBindingHandler<OsuAction> + public class GameplayCursorContainer : CursorContainer, IKeyBindingHandler<OsuAction> { protected override Drawable CreateCursor() => new OsuCursor(); @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly Container<Drawable> fadeContainer; - public GameplayCursor() + public GameplayCursorContainer() { InternalChild = fadeContainer = new Container { @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public OsuCursor() { Origin = Anchor.Centre; - Size = new Vector2(42); + Size = new Vector2(28); } protected override void SkinChanged(ISkinSource skin, bool allowFallback) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 2db2b45540..51733c3c01 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -10,7 +10,9 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using System.Linq; +using osu.Framework.Graphics.Cursor; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.UI.Cursor; namespace osu.Game.Rulesets.Osu.UI { @@ -22,6 +24,12 @@ namespace osu.Game.Rulesets.Osu.UI public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); + private readonly PlayfieldAdjustmentContainer adjustmentContainer; + + protected override Container CursorTargetContainer => adjustmentContainer; + + protected override CursorContainer CreateCursor() => new GameplayCursorContainer(); + public OsuPlayfield() { Anchor = Anchor.Centre; @@ -29,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI Size = new Vector2(0.75f); - InternalChild = new PlayfieldAdjustmentContainer + InternalChild = adjustmentContainer = new PlayfieldAdjustmentContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 85b72cbb5b..81482a9a01 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Graphics.Cursor; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; @@ -13,7 +12,6 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -59,7 +57,5 @@ namespace osu.Game.Rulesets.Osu.UI return first.StartTime - first.TimePreempt; } } - - protected override CursorContainer CreateCursor() => new GameplayCursor(); } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs index 4e5491da9c..5b890b3d03 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModAutoplay : ModAutoplay<TaikoHitObject> { - protected override Score CreateReplayScore(Beatmap<TaikoHitObject> beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, Replay = new TaikoAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 14a6f98480..01ba53e07b 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -9,14 +9,17 @@ using osu.Game.Replays; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Taiko.Beatmaps; namespace osu.Game.Rulesets.Taiko.Replays { - public class TaikoAutoGenerator : AutoGenerator<TaikoHitObject> + public class TaikoAutoGenerator : AutoGenerator { + public new TaikoBeatmap Beatmap => (TaikoBeatmap)base.Beatmap; + private const double swell_hit_speed = 50; - public TaikoAutoGenerator(Beatmap<TaikoHitObject> beatmap) + public TaikoAutoGenerator(IBeatmap beatmap) : base(beatmap) { Replay = new Replay(); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 7851a2f919..08a56488aa 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Replays; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Difficulty; @@ -99,6 +100,11 @@ namespace osu.Game.Rulesets.Taiko new MultiMod(new TaikoModAutoplay(), new ModCinema()), new TaikoModRelax(), }; + case ModType.Fun: + return new Mod[] + { + new MultiMod(new ModWindUp<TaikoHitObject>(), new ModWindDown<TaikoHitObject>()) + }; default: return new Mod[] { }; } diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs index c12015a019..c34190d567 100644 --- a/osu.Game.Tests/Visual/TestCaseReplay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplay.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.ComponentModel; using System.Linq; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual @@ -14,15 +16,27 @@ namespace osu.Game.Tests.Visual { protected override Player CreatePlayer(Ruleset ruleset) { - // We create a dummy RulesetContainer just to get the replay - we don't want to use mods here - // to simulate setting a replay rather than having the replay already set for us - Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); - var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(Beatmap.Value); + var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo); - // Reset the mods - Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Where(m => !(m is ModAutoplay)); + return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod().CreateReplayScore(beatmap)); + } - return new ReplayPlayer(dummyRulesetContainer.ReplayScore); + protected override void AddCheckSteps(Func<Player> player) + { + base.AddCheckSteps(player); + AddUntilStep(() => ((ScoreAccessibleReplayPlayer)player()).ScoreProcessor.TotalScore.Value > 0, "score above zero"); + AddUntilStep(() => ((ScoreAccessibleReplayPlayer)player()).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0), "key counter counted keys"); + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + public new HUDOverlay HUDOverlay => base.HUDOverlay; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score) + { + } } } } diff --git a/osu.Game.Tests/Visual/TestCaseToolbar.cs b/osu.Game.Tests/Visual/TestCaseToolbar.cs index be1b75823a..cb5f33911b 100644 --- a/osu.Game.Tests/Visual/TestCaseToolbar.cs +++ b/osu.Game.Tests/Visual/TestCaseToolbar.cs @@ -24,10 +24,13 @@ namespace osu.Game.Tests.Visual public TestCaseToolbar() { var toolbar = new Toolbar { State = Visibility.Visible }; + ToolbarNotificationButton notificationButton = null; - Add(toolbar); - - var notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First(); + AddStep("create toolbar", () => + { + Add(toolbar); + notificationButton = toolbar.Children.OfType<FillFlowContainer>().Last().Children.OfType<ToolbarNotificationButton>().First(); + }); void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count); diff --git a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs index 3ee617e092..506121efd7 100644 --- a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual { public class TestCaseUpdateableBeatmapBackgroundSprite : OsuTestCase { - private UpdateableBeatmapBackgroundSprite backgroundSprite; + private TestUpdateableBeatmapBackgroundSprite backgroundSprite; [Resolved] private BeatmapManager beatmaps { get; set; } @@ -28,30 +28,36 @@ namespace osu.Game.Tests.Visual var imported = ImportBeatmapTest.LoadOszIntoOsu(osu); - Child = backgroundSprite = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }; + Child = backgroundSprite = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both }; backgroundSprite.Beatmap.BindTo(beatmapBindable); var req = new GetBeatmapSetRequest(1); api.Queue(req); - AddStep("null", () => beatmapBindable.Value = null); - - AddStep("imported", () => beatmapBindable.Value = imported.Beatmaps.First()); + AddStep("load null beatmap", () => beatmapBindable.Value = null); + AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup..."); + AddStep("load imported beatmap", () => beatmapBindable.Value = imported.Beatmaps.First()); + AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup..."); if (api.IsLoggedIn) { AddUntilStep(() => req.Result != null, "wait for api response"); - - AddStep("online", () => beatmapBindable.Value = new BeatmapInfo + AddStep("load online beatmap", () => beatmapBindable.Value = new BeatmapInfo { BeatmapSet = req.Result?.ToBeatmapSet(rulesets) }); + AddUntilStep(() => backgroundSprite.ChildCount == 1, "wait for cleanup..."); } else { AddStep("online (login first)", () => { }); } } + + private class TestUpdateableBeatmapBackgroundSprite : UpdateableBeatmapBackgroundSprite + { + public int ChildCount => InternalChildren.Count; + } } } diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index 012926fb0e..f0af09459f 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Beatmaps.Drawables { /// <summary> - /// Display a baetmap background from a local source, but fallback to online source if not available. + /// Display a beatmap background from a local source, but fallback to online source if not available. /// </summary> public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable<BeatmapInfo> { @@ -26,37 +26,51 @@ namespace osu.Game.Beatmaps.Drawables this.beatmapSetCoverType = beatmapSetCoverType; } - protected override Drawable CreateDrawable(BeatmapInfo model) + private BeatmapInfo lastModel; + + protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Drawable content, double timeBeforeLoad) { return new DelayedLoadUnloadWrapper(() => { - Drawable drawable; + // If DelayedLoadUnloadWrapper is attempting to RELOAD the same content (Beatmap), that means that it was + // previously UNLOADED and thus its children have been disposed of, so we need to recreate them here. + if (lastModel == Beatmap.Value && Beatmap.Value != null) + return CreateDrawable(Beatmap.Value); - var localBeatmap = beatmaps.GetWorkingBeatmap(model); - - if (model?.BeatmapSet?.OnlineInfo != null) - drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); - else if (localBeatmap.BeatmapInfo.ID != 0) - { - // Fall back to local background if one exists - drawable = new BeatmapBackgroundSprite(localBeatmap); - } - else - { - // Use the default background if somehow an online set does not exist and we don't have a local copy. - drawable = new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap); - } - - drawable.RelativeSizeAxes = Axes.Both; - drawable.Anchor = Anchor.Centre; - drawable.Origin = Anchor.Centre; - drawable.FillMode = FillMode.Fill; - drawable.OnLoadComplete = d => d.FadeInFromZero(400); - - return drawable; - }, 500, 10000); + // If the model has changed since the previous unload (or if there was no load), then we can safely use the given content + lastModel = Beatmap.Value; + return content; + }, timeBeforeLoad, 10000); } - protected override double FadeDuration => 0; + protected override Drawable CreateDrawable(BeatmapInfo model) + { + Drawable drawable; + + var localBeatmap = beatmaps.GetWorkingBeatmap(model); + + if (model?.BeatmapSet?.OnlineInfo != null) + { + drawable = new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); + } + else if (localBeatmap.BeatmapInfo.ID != 0) + { + // Fall back to local background if one exists + drawable = new BeatmapBackgroundSprite(localBeatmap); + } + else + { + // Use the default background if somehow an online set does not exist and we don't have a local copy. + drawable = new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap); + } + + drawable.RelativeSizeAxes = Axes.Both; + drawable.Anchor = Anchor.Centre; + drawable.Origin = Anchor.Centre; + drawable.FillMode = FillMode.Fill; + drawable.OnLoadComplete = d => d.FadeInFromZero(400); + + return drawable; + } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 4d039e0b8a..5593abf348 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -70,13 +70,15 @@ namespace osu.Game.Online.API internal new void Schedule(Action action) => base.Schedule(action); + /// <summary> + /// Register a component to receive API events. + /// Fires <see cref="IOnlineComponent.APIStateChanged"/> once immediately to ensure a correct state. + /// </summary> + /// <param name="component"></param> public void Register(IOnlineComponent component) { - Scheduler.Add(delegate - { - components.Add(component); - component.APIStateChanged(this, state); - }); + Scheduler.Add(delegate { components.Add(component); }); + component.APIStateChanged(this, state); } public void Unregister(IOnlineComponent component) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 61b2014af8..dca0226499 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Toolbar public Action OnHome; - private readonly ToolbarUserArea userArea; + private ToolbarUserArea userArea; protected override bool BlockPositionalInput => false; @@ -34,6 +34,13 @@ namespace osu.Game.Overlays.Toolbar private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All); public Toolbar() + { + RelativeSizeAxes = Axes.X; + Size = new Vector2(1, HEIGHT); + } + + [BackgroundDependencyLoader(true)] + private void load(OsuGame osuGame) { Children = new Drawable[] { @@ -76,13 +83,6 @@ namespace osu.Game.Overlays.Toolbar } }; - RelativeSizeAxes = Axes.X; - Size = new Vector2(1, HEIGHT); - } - - [BackgroundDependencyLoader(true)] - private void load(OsuGame osuGame) - { StateChanged += visibility => { if (overlayActivationMode.Value == OverlayActivation.Disabled) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 22e70b446e..44c78f8436 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -14,10 +14,6 @@ namespace osu.Game.Rulesets.Mods public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToRulesetContainer<T> where T : HitObject { - protected virtual Score CreateReplayScore(Beatmap<T> beatmap) => new Score { Replay = new Replay() }; - - public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; - public virtual void ApplyToRulesetContainer(RulesetContainer<T> rulesetContainer) => rulesetContainer.SetReplayScore(CreateReplayScore(rulesetContainer.Beatmap)); } @@ -31,5 +27,9 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; public bool AllowFail => false; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; + + public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; + + public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() }; } } diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index b69019cd91..e59654c60d 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override string Description => "Zoooooooooom..."; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime) }; + public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime), typeof(ModTimeRamp) }; public virtual void ApplyToClock(IAdjustableClock clock) { diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 1cffa37b04..07cceb6f49 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override string Description => "Less zoom..."; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime) }; + public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime), typeof(ModTimeRamp) }; public virtual void ApplyToClock(IAdjustableClock clock) { diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs new file mode 100644 index 0000000000..4a0ed0f9bb --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Audio; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModTimeRamp : Mod + { + public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime), typeof(ModHalfTime) }; + + protected abstract double FinalRateAdjustment { get; } + } + + public abstract class ModTimeRamp<T> : ModTimeRamp, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap<T> + where T : HitObject + { + private double finalRateTime; + + private double beginRampTime; + + private IAdjustableClock clock; + + private IHasPitchAdjust pitchAdjust; + + /// <summary> + /// The point in the beatmap at which the final ramping rate should be reached. + /// </summary> + private const double final_rate_progress = 0.75f; + + public virtual void ApplyToClock(IAdjustableClock clock) + { + this.clock = clock; + pitchAdjust = (IHasPitchAdjust)clock; + + // for preview purposes + pitchAdjust.PitchAdjust = 1.0 + FinalRateAdjustment; + } + + public virtual void ApplyToBeatmap(Beatmap<T> beatmap) + { + HitObject lastObject = beatmap.HitObjects.LastOrDefault(); + + beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0; + finalRateTime = final_rate_progress * ((lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0); + } + + public virtual void Update(Playfield playfield) + { + var absRate = Math.Abs(FinalRateAdjustment); + var adjustment = MathHelper.Clamp(absRate * ((clock.CurrentTime - beginRampTime) / finalRateTime), 0, absRate); + + pitchAdjust.PitchAdjust = 1 + Math.Sign(FinalRateAdjustment) * adjustment; + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs new file mode 100644 index 0000000000..646c5c64e4 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Mods +{ + public class ModWindDown<T> : ModTimeRamp<T> + where T : HitObject + { + public override string Name => "Wind Down"; + public override string Acronym => "WD"; + public override string Description => "Sloooow doooown..."; + public override FontAwesome Icon => FontAwesome.fa_chevron_circle_down; + public override double ScoreMultiplier => 1.0; + protected override double FinalRateAdjustment => -0.25; + } +} diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs new file mode 100644 index 0000000000..9050b5591a --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Mods +{ + public class ModWindUp<T> : ModTimeRamp<T> + where T : HitObject + { + public override string Name => "Wind Up"; + public override string Acronym => "WU"; + public override string Description => "Can you keep up?"; + public override FontAwesome Icon => FontAwesome.fa_chevron_circle_up; + public override double ScoreMultiplier => 1.0; + protected override double FinalRateAdjustment => 0.5; + } +} \ No newline at end of file diff --git a/osu.Game/Rulesets/Replays/AutoGenerator.cs b/osu.Game/Rulesets/Replays/AutoGenerator.cs index efca0a5883..1d4cdbf04c 100644 --- a/osu.Game/Rulesets/Replays/AutoGenerator.cs +++ b/osu.Game/Rulesets/Replays/AutoGenerator.cs @@ -1,14 +1,12 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Objects; using osu.Game.Beatmaps; using osu.Game.Replays; namespace osu.Game.Rulesets.Replays { - public abstract class AutoGenerator<T> : IAutoGenerator - where T : HitObject + public abstract class AutoGenerator : IAutoGenerator { /// <summary> /// Creates the auto replay and returns it. @@ -21,11 +19,11 @@ namespace osu.Game.Rulesets.Replays /// <summary> /// The beatmap we're making. /// </summary> - protected Beatmap<T> Beatmap; + protected IBeatmap Beatmap; #endregion - protected AutoGenerator(Beatmap<T> beatmap) + protected AutoGenerator(IBeatmap beatmap) { Beatmap = beatmap; } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index ffab0abebf..70f15b99bd 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets /// <returns>An enumerable of constructed <see cref="Mod"/>s</returns> public virtual IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods) => new Mod[] { }; - public Mod GetAutoplayMod() => GetAllMods().First(mod => mod is ModAutoplay); + public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().First(); protected Ruleset(RulesetInfo rulesetInfo = null) { diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 3b8a7353c6..78d14a27e3 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osuTK; @@ -63,6 +64,10 @@ namespace osu.Game.Rulesets.UI private void load(IBindable<WorkingBeatmap> beatmap) { this.beatmap = beatmap.Value; + + Cursor = CreateCursor(); + if (Cursor != null) + CursorTargetContainer.Add(Cursor); } /// <summary> @@ -82,6 +87,23 @@ namespace osu.Game.Rulesets.UI /// <param name="h">The DrawableHitObject to remove.</param> public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h); + /// <summary> + /// The cursor currently being used by this <see cref="Playfield"/>. May be null if no cursor is provided. + /// </summary> + public CursorContainer Cursor { get; private set; } + + /// <summary> + /// Provide an optional cursor which is to be used for gameplay. + /// If providing a cursor, <see cref="CursorTargetContainer"/> must also point to a valid target container. + /// </summary> + /// <returns>The cursor, or null if a cursor is not rqeuired.</returns> + protected virtual CursorContainer CreateCursor() => null; + + /// <summary> + /// The target container to add the cursor after it is created. + /// </summary> + protected virtual Container CursorTargetContainer => null; + /// <summary> /// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>. /// This does not add the <see cref="Playfield"/> to the draw hierarchy. diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 1c29cf4e2b..ed5f23dc7f 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -16,7 +16,9 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Cursor; using osu.Framework.Input; +using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Game.Graphics.Cursor; using osu.Game.Input.Handlers; using osu.Game.Overlays; using osu.Game.Replays; @@ -32,7 +34,7 @@ namespace osu.Game.Rulesets.UI /// Should not be derived - derive <see cref="RulesetContainer{TObject}"/> instead. /// </para> /// </summary> - public abstract class RulesetContainer : Container + public abstract class RulesetContainer : Container, IProvideCursor { /// <summary> /// The selected variant. @@ -74,10 +76,11 @@ namespace osu.Game.Rulesets.UI /// </summary> public Container Overlays { get; protected set; } - /// <summary> - /// The cursor provided by this <see cref="RulesetContainer"/>. May be null if no cursor is provided. - /// </summary> - public readonly CursorContainer Cursor; + public CursorContainer Cursor => Playfield.Cursor; + + public bool ProvidingUserCursor => Playfield.Cursor != null && !HasReplayLoaded.Value; + + protected override bool OnHover(HoverEvent e) => true; // required for IProvideCursor public readonly Ruleset Ruleset; @@ -101,8 +104,6 @@ namespace osu.Game.Rulesets.UI KeyBindingInputManager.UseParentInput = !paused.NewValue; }; - - Cursor = CreateCursor(); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -259,9 +260,6 @@ namespace osu.Game.Rulesets.UI Playfield }); - if (Cursor != null) - KeyBindingInputManager.Add(Cursor); - InternalChildren = new Drawable[] { KeyBindingInputManager, diff --git a/osu.Game/Screens/Multi/Components/ParticipantCount.cs b/osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/ParticipantCount.cs rename to osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index dd1e060125..7cbae611ea 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -107,6 +107,14 @@ namespace osu.Game.Screens.Multi.Lounge Filter.Search.HoldFocus = false; } + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + + if (currentRoom.Value?.RoomID.Value == null) + currentRoom.Value = new Room(); + } + private void joinRequested(Room room) { processingOverlay.Show(); diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs index 6a6a1f274c..e1592532a3 100644 --- a/osu.Game/Screens/Multi/Match/Components/Header.cs +++ b/osu.Game/Screens/Multi/Match/Components/Header.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -13,6 +14,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Overlays.SearchableList; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Play.HUD; using osuTK; @@ -108,7 +110,7 @@ namespace osu.Game.Screens.Multi.Match.Components }, }; - CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods, true); + CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods ?? Enumerable.Empty<Mod>(), true); beatmapButton.Action = () => RequestBeatmapSelection?.Invoke(); } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs new file mode 100644 index 0000000000..594df63420 --- /dev/null +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -0,0 +1,151 @@ +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework; +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.Configuration; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.Play +{ + /// <summary> + /// Encapsulates gameplay timing logic and provides a <see cref="GameplayClock"/> for children. + /// </summary> + public class GameplayClockContainer : Container + { + private readonly WorkingBeatmap beatmap; + + /// <summary> + /// The original source (usually a <see cref="WorkingBeatmap"/>'s track). + /// </summary> + private readonly IAdjustableClock sourceClock; + + public readonly BindableBool IsPaused = new BindableBool(); + + /// <summary> + /// The decoupled clock used for gameplay. Should be used for seeks and clock control. + /// </summary> + private readonly DecoupleableInterpolatingFramedClock adjustableClock; + + public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1) + { + Default = 1, + MinValue = 0.5, + MaxValue = 2, + Precision = 0.1, + }; + + /// <summary> + /// The final clock which is exposed to underlying components. + /// </summary> + [Cached] + private readonly GameplayClock gameplayClock; + + private Bindable<double> userAudioOffset; + + private readonly FramedOffsetClock offsetClock; + + public GameplayClockContainer(WorkingBeatmap beatmap, bool allowLeadIn, double gameplayStartTime) + { + this.beatmap = beatmap; + + RelativeSizeAxes = Axes.Both; + + sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock(); + + adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + + adjustableClock.Seek(allowLeadIn + ? Math.Min(0, gameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn) + : gameplayStartTime); + + adjustableClock.ProcessFrame(); + + // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 }; + + // the final usable gameplay clock with user-set offsets applied. + offsetClock = new FramedOffsetClock(platformOffsetClock); + + // the clock to be exposed via DI to children. + gameplayClock = new GameplayClock(offsetClock); + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset); + userAudioOffset.BindValueChanged(offset => offsetClock.Offset = offset.NewValue, true); + + UserPlaybackRate.ValueChanged += _ => updateRate(); + } + + public void Restart() + { + Task.Run(() => + { + sourceClock.Reset(); + + Schedule(() => + { + adjustableClock.ChangeSource(sourceClock); + updateRate(); + + this.Delay(750).Schedule(() => + { + if (!IsPaused.Value) + { + adjustableClock.Start(); + } + }); + }); + }); + } + + public void Start() + { + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the audio clock source potentially taking time to enter a completely stopped state + adjustableClock.Seek(adjustableClock.CurrentTime); + adjustableClock.Start(); + } + + public void Seek(double time) => adjustableClock.Seek(time); + + public void Stop() => adjustableClock.Stop(); + + public void ResetLocalAdjustments() + { + // In the case of replays, we may have changed the playback rate. + UserPlaybackRate.Value = 1; + } + + protected override void Update() + { + if (!IsPaused.Value) + offsetClock.ProcessFrame(); + + base.Update(); + } + + private void updateRate() + { + if (sourceClock == null) return; + + sourceClock.Rate = 1; + foreach (var mod in beatmap.Mods.Value.OfType<IApplicableToClock>()) + mod.ApplyToClock(sourceClock); + + sourceClock.Rate *= UserPlaybackRate.Value; + } + } +} diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index d3dba88281..e99f6d836e 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -19,7 +19,9 @@ namespace osu.Game.Screens.Play.HUD public readonly PlaybackSettings PlaybackSettings; public readonly VisualSettings VisualSettings; + //public readonly CollectionSettings CollectionSettings; + //public readonly DiscussionSettings DiscussionSettings; public PlayerSettingsOverlay() diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index a19b0d1e5c..4fd0572c1a 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -1,12 +1,12 @@ // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -40,7 +40,9 @@ namespace osu.Game.Screens.Play private static bool hasShownNotificationOnce; - public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IAdjustableClock adjustableClock) + public Action<double> RequestSeek; + + public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working) { RelativeSizeAxes = Axes.Both; @@ -92,11 +94,9 @@ namespace osu.Game.Screens.Play Progress.Objects = rulesetContainer.Objects; Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value; - Progress.RequestSeek = pos => adjustableClock.Seek(pos); + Progress.RequestSeek = time => RequestSeek(time); ModDisplay.Current.BindTo(working.Mods); - - PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Screens/Play/PausableGameplayContainer.cs b/osu.Game/Screens/Play/PausableGameplayContainer.cs index fa475deb34..99f0083b55 100644 --- a/osu.Game/Screens/Play/PausableGameplayContainer.cs +++ b/osu.Game/Screens/Play/PausableGameplayContainer.cs @@ -7,16 +7,13 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Timing; using osu.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Screens.Play { /// <summary> - /// A container which handles pausing children, displaying a pause overlay with choices and processing the clock. - /// Exposes a <see cref="GameplayClock"/> to children via DI. - /// This alleviates a lot of the intricate pause logic from being in <see cref="Player"/> + /// A container which handles pausing children, displaying an overlay blocking its children during paused state. /// </summary> public class PausableGameplayContainer : Container { @@ -44,46 +41,33 @@ namespace osu.Game.Screens.Play public Action OnRetry; public Action OnQuit; - private readonly FramedClock offsetClock; - private readonly DecoupleableInterpolatingFramedClock adjustableClock; - - /// <summary> - /// The final clock which is exposed to underlying components. - /// </summary> - [Cached] - private readonly GameplayClock gameplayClock; + public Action Stop; + public Action Start; /// <summary> /// Creates a new <see cref="PausableGameplayContainer"/>. /// </summary> - /// <param name="offsetClock">The gameplay clock. This is the clock that will process frames. Includes user/system offsets.</param> - /// <param name="adjustableClock">The seekable clock. This is the clock that will be paused and resumed. Should not be processed (it is processed automatically by <see cref="offsetClock"/>).</param> - public PausableGameplayContainer(FramedClock offsetClock, DecoupleableInterpolatingFramedClock adjustableClock) + public PausableGameplayContainer() { - this.offsetClock = offsetClock; - this.adjustableClock = adjustableClock; - - gameplayClock = new GameplayClock(offsetClock); - RelativeSizeAxes = Axes.Both; - AddInternal(content = new Container + InternalChildren = new[] { - Clock = this.offsetClock, - ProcessCustomClock = false, - RelativeSizeAxes = Axes.Both - }); - - AddInternal(pauseOverlay = new PauseOverlay - { - OnResume = () => + content = new Container { - IsResuming = true; - this.Delay(400).Schedule(Resume); + RelativeSizeAxes = Axes.Both }, - OnRetry = () => OnRetry(), - OnQuit = () => OnQuit(), - }); + pauseOverlay = new PauseOverlay + { + OnResume = () => + { + IsResuming = true; + this.Delay(400).Schedule(Resume); + }, + OnRetry = () => OnRetry(), + OnQuit = () => OnQuit(), + } + }; } public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called. @@ -93,7 +77,7 @@ namespace osu.Game.Screens.Play if (IsPaused.Value) return; // stop the seekable clock (stops the audio eventually) - adjustableClock.Stop(); + Stop?.Invoke(); IsPaused.Value = true; pauseOverlay.Show(); @@ -105,14 +89,12 @@ namespace osu.Game.Screens.Play { if (!IsPaused.Value) return; - IsPaused.Value = false; IsResuming = false; lastPauseActionTime = Time.Current; - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the audio clock source potentially taking time to enter a completely stopped state - adjustableClock.Seek(adjustableClock.CurrentTime); - adjustableClock.Start(); + IsPaused.Value = false; + + Start?.Invoke(); pauseOverlay.Hide(); } @@ -131,9 +113,6 @@ namespace osu.Game.Screens.Play if (!game.IsActive.Value && CanPause) Pause(); - if (!IsPaused.Value) - offsetClock.ProcessFrame(); - base.Update(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 44707c74f5..ced0a43679 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -3,24 +3,19 @@ using System; using System.Linq; -using System.Threading.Tasks; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Cursor; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -34,7 +29,7 @@ using osu.Game.Storyboards.Drawables; namespace osu.Game.Screens.Play { - public class Player : ScreenWithBeatmapBackground, IProvideCursor + public class Player : ScreenWithBeatmapBackground { protected override bool AllowBackButton => false; // handled by HoldForMenuButton @@ -53,22 +48,11 @@ namespace osu.Game.Screens.Play public bool AllowResults { get; set; } = true; private Bindable<bool> mouseWheelDisabled; - private Bindable<double> userAudioOffset; private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>(); public int RestartCount; - public CursorContainer Cursor => RulesetContainer.Cursor; - public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value; - - private IAdjustableClock sourceClock; - - /// <summary> - /// The decoupled clock used for gameplay. Should be used for seeks and clock control. - /// </summary> - private DecoupleableInterpolatingFramedClock adjustableClock; - [Resolved] private ScoreManager scoreManager { get; set; } @@ -98,25 +82,113 @@ namespace osu.Game.Screens.Play public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true; + private GameplayClockContainer gameplayClockContainer; + [BackgroundDependencyLoader] private void load(AudioManager audio, APIAccess api, OsuConfigManager config) { this.api = api; - WorkingBeatmap working = Beatmap.Value; - if (working is DummyWorkingBeatmap) + WorkingBeatmap working = loadBeatmap(); + + if (working == null) return; sampleRestart = audio.Sample.Get(@"Gameplay/restart"); mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel); - userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset); - IBeatmap beatmap; + ScoreProcessor = RulesetContainer.CreateScoreProcessor(); + if (!ScoreProcessor.Mode.Disabled) + config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); + + InternalChild = gameplayClockContainer = new GameplayClockContainer(working, AllowLeadIn, RulesetContainer.GameplayStartTime); + + gameplayClockContainer.Children = new Drawable[] + { + PausableGameplayContainer = new PausableGameplayContainer + { + Retries = RestartCount, + OnRetry = restart, + OnQuit = performUserRequestedExit, + Start = gameplayClockContainer.Start, + Stop = gameplayClockContainer.Stop, + IsPaused = { BindTarget = gameplayClockContainer.IsPaused }, + CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value, + Children = new[] + { + StoryboardContainer = CreateStoryboardContainer(), + new ScalingContainer(ScalingMode.Gameplay) + { + Child = new LocalSkinOverrideContainer(working.Skin) + { + RelativeSizeAxes = Axes.Both, + Child = RulesetContainer + } + }, + new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Breaks = working.Beatmap.Breaks + }, + // display the cursor above some HUD elements. + RulesetContainer.Cursor?.CreateProxy() ?? new Container(), + HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working) + { + HoldToQuit = { Action = performUserRequestedExit }, + PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = gameplayClockContainer.UserPlaybackRate } } }, + KeyCounter = { Visible = { BindTarget = RulesetContainer.HasReplayLoaded } }, + RequestSeek = gameplayClockContainer.Seek, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }, + new SkipOverlay(RulesetContainer.GameplayStartTime) + { + RequestSeek = gameplayClockContainer.Seek + }, + } + }, + failOverlay = new FailOverlay + { + OnRetry = restart, + OnQuit = performUserRequestedExit, + }, + new HotkeyRetryOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + fadeOut(true); + restart(); + }, + } + }; + + // bind clock into components that require it + RulesetContainer.IsPaused.BindTo(gameplayClockContainer.IsPaused); + + if (ShowStoryboard.Value) + initializeStoryboard(false); + + // Bind ScoreProcessor to ourselves + ScoreProcessor.AllJudged += onCompletion; + ScoreProcessor.Failed += onFail; + + foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>()) + mod.ApplyToScoreProcessor(ScoreProcessor); + } + + private WorkingBeatmap loadBeatmap() + { + WorkingBeatmap working = Beatmap.Value; + if (working is DummyWorkingBeatmap) + return null; try { - beatmap = working.Beatmap; + var beatmap = working.Beatmap; if (beatmap == null) throw new InvalidOperationException("Beatmap was not loaded"); @@ -140,119 +212,17 @@ namespace osu.Game.Screens.Play if (!RulesetContainer.Objects.Any()) { Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error); - return; + return null; } } catch (Exception e) { Logger.Error(e, "Could not load beatmap sucessfully!"); //couldn't load, hard abort! - return; + return null; } - sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock(); - adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - - adjustableClock.Seek(AllowLeadIn - ? Math.Min(0, RulesetContainer.GameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn) - : RulesetContainer.GameplayStartTime); - - adjustableClock.ProcessFrame(); - - // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 }; - - // the final usable gameplay clock with user-set offsets applied. - var offsetClock = new FramedOffsetClock(platformOffsetClock); - - userAudioOffset.ValueChanged += offset => offsetClock.Offset = offset.NewValue; - userAudioOffset.TriggerChange(); - - ScoreProcessor = RulesetContainer.CreateScoreProcessor(); - if (!ScoreProcessor.Mode.Disabled) - config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); - - InternalChildren = new Drawable[] - { - PausableGameplayContainer = new PausableGameplayContainer(offsetClock, adjustableClock) - { - Retries = RestartCount, - OnRetry = restart, - OnQuit = performUserRequestedExit, - CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value, - Children = new Container[] - { - StoryboardContainer = CreateStoryboardContainer(), - new ScalingContainer(ScalingMode.Gameplay) - { - Child = new LocalSkinOverrideContainer(working.Skin) - { - RelativeSizeAxes = Axes.Both, - Child = RulesetContainer - } - }, - new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Breaks = beatmap.Breaks - }, - new ScalingContainer(ScalingMode.Gameplay) - { - Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(), - }, - HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, adjustableClock) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - new SkipOverlay(RulesetContainer.GameplayStartTime) - { - RequestSeek = time => adjustableClock.Seek(time) - }, - } - }, - failOverlay = new FailOverlay - { - OnRetry = restart, - OnQuit = performUserRequestedExit, - }, - new HotkeyRetryOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - fadeOut(true); - restart(); - }, - } - }; - - HUDOverlay.HoldToQuit.Action = performUserRequestedExit; - HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded); - - RulesetContainer.IsPaused.BindTo(PausableGameplayContainer.IsPaused); - - if (ShowStoryboard.Value) - initializeStoryboard(false); - - // Bind ScoreProcessor to ourselves - ScoreProcessor.AllJudged += onCompletion; - ScoreProcessor.Failed += onFail; - - foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToScoreProcessor>()) - mod.ApplyToScoreProcessor(ScoreProcessor); - } - - private void applyRateFromMods() - { - if (sourceClock == null) return; - - sourceClock.Rate = 1; - foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToClock>()) - mod.ApplyToClock(sourceClock); + return working; } private void performUserRequestedExit() @@ -321,7 +291,7 @@ namespace osu.Game.Screens.Play if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail)) return false; - adjustableClock.Stop(); + gameplayClockContainer.Stop(); HasFailed = true; failOverlay.Retries = RestartCount; @@ -355,24 +325,7 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; - Task.Run(() => - { - sourceClock.Reset(); - - Schedule(() => - { - adjustableClock.ChangeSource(sourceClock); - applyRateFromMods(); - - this.Delay(750).Schedule(() => - { - if (!PausableGameplayContainer.IsPaused.Value) - { - adjustableClock.Start(); - } - }); - }); - }); + gameplayClockContainer.Restart(); PausableGameplayContainer.Alpha = 0; PausableGameplayContainer.FadeIn(750, Easing.OutQuint); @@ -395,8 +348,8 @@ namespace osu.Game.Screens.Play if ((!AllowPause || HasFailed || !ValidForResume || PausableGameplayContainer?.IsPaused.Value != false || RulesetContainer?.HasReplayLoaded.Value != false) && (!PausableGameplayContainer?.IsResuming ?? true)) { - // In the case of replays, we may have changed the playback rate. - applyRateFromMods(); + gameplayClockContainer.ResetLocalAdjustments(); + fadeOut(); return base.OnExiting(next); } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index ebbed299f7..c691d161ed 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -4,7 +4,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -16,7 +15,13 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override string Title => @"playback"; - public IAdjustableClock AdjustableClock { set; get; } + public readonly Bindable<double> UserPlaybackRate = new BindableDouble(1) + { + Default = 1, + MinValue = 0.5, + MaxValue = 2, + Precision = 0.1, + }; private readonly PlayerSliderBar<double> rateSlider; @@ -47,31 +52,13 @@ namespace osu.Game.Screens.Play.PlayerSettings } }, }, - rateSlider = new PlayerSliderBar<double> - { - Bindable = new BindableDouble(1) - { - Default = 1, - MinValue = 0.5, - MaxValue = 2, - Precision = 0.1, - }, - } + rateSlider = new PlayerSliderBar<double> { Bindable = UserPlaybackRate } }; } protected override void LoadComplete() { base.LoadComplete(); - - if (AdjustableClock == null) - return; - - var clockRate = AdjustableClock.Rate; - - // can't trigger this line instantly as the underlying clock may not be ready to accept adjustments yet. - rateSlider.Bindable.ValueChanged += multiplier => AdjustableClock.Rate = clockRate * multiplier.NewValue; - rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); } } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 3dc152eebe..fa5dc4c1d1 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -64,7 +65,7 @@ namespace osu.Game.Screens.Select { Ruleset.Value = CurrentItem.Value.Ruleset; Beatmap.Value = beatmaps.GetWorkingBeatmap(CurrentItem.Value.Beatmap); - Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods; + Beatmap.Value.Mods.Value = selectedMods.Value = CurrentItem.Value.RequiredMods ?? Enumerable.Empty<Mod>(); } Beatmap.Disabled = true; diff --git a/osu.Game/Utils/RavenLogger.cs b/osu.Game/Utils/RavenLogger.cs index acc3ab28b5..7f4faa60ae 100644 --- a/osu.Game/Utils/RavenLogger.cs +++ b/osu.Game/Utils/RavenLogger.cs @@ -20,14 +20,14 @@ namespace osu.Game.Utils private readonly List<Task> tasks = new List<Task>(); - private Exception lastException; - public RavenLogger(OsuGame game) { raven.Release = game.Version; if (!game.IsDeployedBuild) return; + Exception lastException = null; + Logger.NewEntry += entry => { if (entry.Level < LogLevel.Verbose) return; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 882c6ef064..2e945c212d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -16,7 +16,7 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.128.0" /> - <PackageReference Include="ppy.osu.Framework" Version="2019.307.0" /> + <PackageReference Include="ppy.osu.Framework" Version="2019.308.0" /> <PackageReference Include="SharpCompress" Version="0.22.0" /> <PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" /> diff --git a/osu.iOS.props b/osu.iOS.props index bc3e553fef..b25e2a8bb2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -105,8 +105,8 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.128.0" /> - <PackageReference Include="ppy.osu.Framework" Version="2019.307.0" /> - <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.307.0" /> + <PackageReference Include="ppy.osu.Framework" Version="2019.308.0" /> + <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.308.0" /> <PackageReference Include="SharpCompress" Version="0.22.0" /> <PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />