diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 184938ceda..bc2f632873 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneOsuAnalysisContainer : OsuTestScene { private TestReplayAnalysisOverlay analysisContainer = null!; - private ReplayAnalysisSettings settings = null!; + private OsuReplayAnalysisSettings settings = null!; [Cached] private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo); @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), }, - settings = new ReplayAnalysisSettings(config), + settings = new TestOsuReplayAnalysisSettings(Ruleset.Value.CreateInstance(), config), }; settings.ShowClickMarkers.Value = false; @@ -129,5 +129,26 @@ namespace osu.Game.Rulesets.Osu.Tests public bool AimMarkersVisible => FrameMarkers?.Alpha > 0 && FrameMarkers.Entries.Any(); public bool AimLinesVisible => CursorPath?.Alpha > 0 && CursorPath.Vertices.Count > 1; } + + private partial class TestOsuReplayAnalysisSettings : OsuReplayAnalysisSettings + { + private readonly OsuRulesetConfigManager config; + + public TestOsuReplayAnalysisSettings(Ruleset ruleset, OsuRulesetConfigManager config) + : base(ruleset) + { + this.config = config; + } + + [BackgroundDependencyLoader] + private void load() + { + config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, ShowClickMarkers); + config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers); + config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath); + config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor); + config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, DisplayLength); + } + } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 25b1dd9b12..6321824c0f 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -382,5 +382,12 @@ namespace osu.Game.Rulesets.Osu } public override bool EditorShowScrollSpeed => false; + + public override ReplayAnalysisSettings CreateReplayAnalysisSettings() + { + var settings = new OsuReplayAnalysisSettings(this); + settings.Expanded.Value = false; + return settings; + } } } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index ab69b67051..33fe151c34 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -40,14 +40,13 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load(ReplayPlayer? replayPlayer) + private void load(Player? player) { - if (replayPlayer != null) + if (player is ReplayPlayer || player is SpectatorPlayer) { ReplayAnalysisOverlay analysisOverlay; - PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); + PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(player.Score.Replay)); Overlays.Add(analysisOverlay.CreateProxy().With(p => p.Depth = float.NegativeInfinity)); - replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuReplayAnalysisSettings.cs similarity index 71% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs rename to osu.Game.Rulesets.Osu/UI/OsuReplayAnalysisSettings.cs index dc4730d76a..52c5529e79 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuReplayAnalysisSettings.cs @@ -5,13 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { - public partial class ReplayAnalysisSettings : PlayerSettingsGroup + public partial class OsuReplayAnalysisSettings : ReplayAnalysisSettings { - private readonly OsuRulesetConfigManager config; + protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; [SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool ShowClickMarkers { get; } = new BindableBool(); @@ -34,22 +35,19 @@ namespace osu.Game.Rulesets.Osu.UI Precision = 200, }; - public ReplayAnalysisSettings(OsuRulesetConfigManager config) - : base("Analysis Settings") + public OsuReplayAnalysisSettings(Ruleset ruleset) + : base(ruleset) { - this.config = config; } [BackgroundDependencyLoader] private void load() { - AddRange(this.CreateSettingsControls()); - - config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, ShowClickMarkers); - config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers); - config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath); - config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor); - config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, DisplayLength); + Config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, ShowClickMarkers); + Config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers); + Config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath); + Config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor); + Config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, DisplayLength); } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 2b7f6c9fc9..0fe9dc9628 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.UI private readonly Replay replay; + private int replayFrameIndex; + public ReplayAnalysisOverlay(Replay replay) { RelativeSizeAxes = Axes.Both; @@ -52,20 +54,39 @@ namespace osu.Game.Rulesets.Osu.UI displayLength.BindValueChanged(_ => { // Need to fully reload to make this work. - loaded.Invalidate(); + invalidateLoaded(); }, true); } - private readonly Cached loaded = new Cached(); + /// + /// Invalidated when containers are not loaded nor loading, false if loading, and true if loaded + /// + /// + /// Knowing the loading/loaded state is for avoiding an enumeration error when adding + /// new entries and not starting a new load while loading + /// + private readonly Cached loadState = new Cached(); private CancellationTokenSource? generationCancellationSource; + private void invalidateLoaded() + { + loadState.Invalidate(); + replayFrameIndex = 0; + } + protected override void Update() { base.Update(); if (requireDisplay) + { initialise(); + // adding entries while the component is asynchronously loading + // can collide with enumeration operations and cause an error + if (loadState.IsValid && loadState.Value) + addEntries(); + } if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0; if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0; @@ -74,10 +95,10 @@ namespace osu.Game.Rulesets.Osu.UI private void initialise() { - if (loaded.IsValid) + if (loadState.IsValid) return; - loaded.Validate(); + loadState.Value = false; generationCancellationSource?.Cancel(); generationCancellationSource = new CancellationTokenSource(); @@ -90,14 +111,23 @@ namespace osu.Game.Rulesets.Osu.UI FrameMarkers = new FrameMarkerContainer(), }; + LoadComponentsAsync(newDrawables, drawables => + { + InternalChildrenEnumerable = drawables; + loadState.Value = true; + }, generationCancellationSource.Token); + } + + private void addEntries() + { bool leftHeld = false; bool rightHeld = false; // This should probably be async as well, but it's a bit of a pain to debounce and everything. // Let's address concerns when they are raised. - foreach (var frame in replay.Frames) + while (replayFrameIndex < replay.Frames.Count) { - var osuFrame = (OsuReplayFrame)frame; + var osuFrame = (OsuReplayFrame)replay.Frames[replayFrameIndex]; bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -107,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.UI else if (!leftHeld && leftButton) { leftHeld = true; - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.LeftButton)); + ClickMarkers!.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.LeftButton)); } if (rightHeld && !rightButton) @@ -115,14 +145,14 @@ namespace osu.Game.Rulesets.Osu.UI else if (!rightHeld && rightButton) { rightHeld = true; - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.RightButton)); + ClickMarkers!.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.RightButton)); } - FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray())); - CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position)); - } + FrameMarkers!.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray())); + CursorPath!.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position)); - LoadComponentsAsync(newDrawables, drawables => InternalChildrenEnumerable = drawables, generationCancellationSource.Token); + replayFrameIndex++; + } } } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index bd1f273b49..749ade9d15 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -426,5 +426,11 @@ namespace osu.Game.Rulesets /// Can be overridden to avoid showing scroll speed changes in the editor. /// public virtual bool EditorShowScrollSpeed => true; + + /// + /// Creates a ruleset-specific replay analysis settings drawable + /// + /// The replay analysis settings drawable + public virtual ReplayAnalysisSettings? CreateReplayAnalysisSettings() => null; } } diff --git a/osu.Game/Rulesets/UI/ReplayAnalysisSettings.cs b/osu.Game/Rulesets/UI/ReplayAnalysisSettings.cs new file mode 100644 index 0000000000..ff7cdfa0e6 --- /dev/null +++ b/osu.Game/Rulesets/UI/ReplayAnalysisSettings.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using osu.Framework.Allocation; +using osu.Game.Configuration; +using osu.Game.Rulesets.Configuration; +using osu.Game.Screens.Play.PlayerSettings; + +namespace osu.Game.Rulesets.UI +{ + public partial class ReplayAnalysisSettings : PlayerSettingsGroup + { + private readonly Ruleset ruleset; + + protected IRulesetConfigManager Config; + + public ReplayAnalysisSettings(Ruleset ruleset) + : base("Analysis Settings") + { + this.ruleset = ruleset; + } + + [BackgroundDependencyLoader] + private void load() + { + AddRange(this.CreateSettingsControls()); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + Config = dependencies.Get().GetConfigFor(ruleset); + if (Config is not null) + dependencies.Cache(Config); + + return dependencies; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 33c3c60ed3..91df739052 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -54,6 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private SpectatorSyncManager syncManager = null!; private PlayerGrid grid = null!; private MultiSpectatorLeaderboard leaderboard = null!; + private FillFlowContainer leaderboardFlow = null!; private PlayerArea? currentAudioSource; private readonly Room room; @@ -76,7 +77,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate [BackgroundDependencyLoader] private void load() { - FillFlowContainer leaderboardFlow; Container scoreDisplayContainer; InternalChildren = new Drawable[] @@ -157,6 +157,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { Expanded = { Value = true }, }, chat => leaderboardFlow.Insert(1, chat)); + + var replayAnalysisSettings = Ruleset.Value.CreateInstance().CreateReplayAnalysisSettings(); + if (replayAnalysisSettings is not null) + leaderboardFlow.Insert(2, replayAnalysisSettings); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 14023bb6ef..3609e000d3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1296,6 +1296,16 @@ namespace osu.Game.Screens.Play } } + /// + /// Create and add to settings overlay. + /// + protected void AddReplayAnalysisSettings() + { + var replayAnalysisSettings = DrawableRuleset.Ruleset.CreateReplayAnalysisSettings(); + if (replayAnalysisSettings is not null) + HUDOverlay.PlayerSettingsOverlay.Add(replayAnalysisSettings); + } + #endregion IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 0c125264a1..366c3d6d20 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -55,16 +55,6 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } - /// - /// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings. - /// - /// The settings group to be shown. - public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => - { - settings.Expanded.Value = false; - HUDOverlay.PlayerSettingsOverlay.Add(settings); - }); - [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -81,6 +71,8 @@ namespace osu.Game.Screens.Play playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); + + AddReplayAnalysisSettings(); } protected override void PrepareReplay() diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index b2ac946642..c87b7fbab1 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -50,6 +50,8 @@ namespace osu.Game.Screens.Play Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }); + + AddReplayAnalysisSettings(); } protected override void LoadComplete()