Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a replay analysis overlay #27334

Merged
merged 46 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b5dbf24
early replay analysis settings version
Sheppsu Feb 1, 2024
d255976
Merge branch 'ppy:master' into replay-analysis-settings
Sheppsu Feb 6, 2024
288eed5
new features + improvements
Sheppsu Feb 10, 2024
1d552e7
move skin component logic
Sheppsu Feb 21, 2024
35b8996
revert mod visibility toggle
Sheppsu Feb 22, 2024
f9d9df3
add test for HitMarkerContainer
Sheppsu Feb 22, 2024
af13389
fix hit marker skinnables
Sheppsu Feb 22, 2024
45444b3
fix formatting issues
Sheppsu Feb 23, 2024
2a1fa8c
fix OsuAnalysisSettings text not updating
Sheppsu Feb 23, 2024
4d669c5
implement pooling
Sheppsu Feb 24, 2024
c95e853
remove old files
Sheppsu Feb 24, 2024
8cdd9c9
skinning changes
Sheppsu Feb 24, 2024
822ecb7
remove unnecessary changes
Sheppsu Feb 24, 2024
29e5f40
remove skinnable
Sheppsu Feb 28, 2024
cefc835
test scene for OsuAnalysisContainer
Sheppsu Feb 28, 2024
7687ab6
fix code formatting
Sheppsu Feb 28, 2024
1ed94e5
Merge branch 'master' into replay-analysis-settings
Sheppsu Sep 1, 2024
a2b15fc
rework code logic to make more sense
Sheppsu Sep 3, 2024
56db29d
make test go indefinitely
Sheppsu Sep 3, 2024
a549cdd
persist analysis settings
Sheppsu Sep 3, 2024
c89597b
fix config mistake
Sheppsu Sep 4, 2024
59ff8c4
fix analysis container creation
Sheppsu Sep 4, 2024
0e16508
Merge branch 'master' into replay-analysis-settings
peppy Sep 4, 2024
a417fec
Move analysis container implementation completely local to osu! ruleset
peppy Sep 4, 2024
992a0da
Rename classes slightly
peppy Sep 4, 2024
cc3d220
Allow settings to be added to replay HUD from ruleset
peppy Sep 4, 2024
9b81deb
Fix settings not working if `ReplayPlayer` is not available
peppy Sep 4, 2024
6c07b87
Isolate configuration container from analysis overlay
peppy Sep 4, 2024
6a30972
Make test more usable
peppy Sep 4, 2024
dcb463a
Split out classes and avoid weird configuration stuff
peppy Sep 4, 2024
7f9a98a
More renames
peppy Sep 4, 2024
a4a37c5
Simplify lifetime entries and stuff
peppy Sep 4, 2024
a6ed719
Visual design pass
peppy Sep 4, 2024
21146c3
Add back shadow cast
peppy Sep 4, 2024
08ebc83
Fix path getting misaligned with negative position values
peppy Sep 5, 2024
ee26ff2
Add out-of-bounds tests to test case
peppy Sep 5, 2024
2d198e5
Second visual design pass
peppy Sep 5, 2024
4f719b9
One more rename pass
peppy Sep 5, 2024
7390d89
Switch to using `CircularProgress` for more consistent sizing
peppy Sep 5, 2024
7983a76
Update test scene to show more button holds (including both buttons s…
peppy Sep 5, 2024
47a9b34
Rename config variables and setting strings
peppy Sep 5, 2024
0f01a85
Add note about cursor hiding being potentially flaky
peppy Sep 5, 2024
a1cf67b
Add setting to adjust replay analysis display length
peppy Sep 5, 2024
167e3a3
Make loading asynchronous
peppy Sep 5, 2024
7136483
Fix nullability inspections
bdach Sep 5, 2024
b9ddac4
Fix test failures
bdach Sep 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Tests.Visual;
using osuTK;

namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneOsuAnalysisContainer : OsuTestScene
{
private TestReplayAnalysisOverlay analysisContainer = null!;
private ReplayAnalysisSettings settings = null!;

[Cached]
private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo);

[SetUpSteps]
public void SetUpSteps()
{
AddStep("create analysis container", () =>
{
Children = new Drawable[]
{
new OsuPlayfieldAdjustmentContainer
{
Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()),
},
settings = new ReplayAnalysisSettings(config),
};

settings.ShowClickMarkers.Value = false;
settings.ShowAimMarkers.Value = false;
settings.ShowCursorPath.Value = false;
});
}

[Test]
public void TestEverythingOn()
{
AddStep("enable everything", () =>
{
settings.ShowClickMarkers.Value = true;
settings.ShowAimMarkers.Value = true;
settings.ShowCursorPath.Value = true;
});
}

[Test]
public void TestHitMarkers()
{
AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true);
AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible);
AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false);
AddUntilStep("hit markers not visible", () => !analysisContainer.HitMarkersVisible);
}

[Test]
public void TestAimMarker()
{
AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true);
AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible);
AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false);
AddUntilStep("aim markers not visible", () => !analysisContainer.AimMarkersVisible);
}

[Test]
public void TestAimLines()
{
AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true);
AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible);
AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false);
AddUntilStep("aim lines not visible", () => !analysisContainer.AimLinesVisible);
}

private Replay fabricateReplay()
{
var frames = new List<ReplayFrame>();
var random = new Random();
int posX = 250;
int posY = 250;

var actions = new HashSet<OsuAction>();

for (int i = 0; i < 1000; i++)
{
posX = Math.Clamp(posX + random.Next(-20, 21), -100, 600);
posY = Math.Clamp(posY + random.Next(-20, 21), -100, 600);

if (random.NextDouble() > (actions.Count == 0 ? 0.9 : 0.95))
{
actions.Add(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton);
}
else if (random.NextDouble() > 0.7)
{
actions.Remove(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton);
}

frames.Add(new OsuReplayFrame
{
Time = Time.Current + i * 15,
Position = new Vector2(posX, posY),
Actions = actions.ToList(),
});
}

return new Replay { Frames = frames };
}

private partial class TestReplayAnalysisOverlay : ReplayAnalysisOverlay
{
public TestReplayAnalysisOverlay(Replay replay)
: base(replay)
{
}

public bool HitMarkersVisible => ClickMarkers?.Alpha > 0 && ClickMarkers.Entries.Any();
public bool AimMarkersVisible => FrameMarkers?.Alpha > 0 && FrameMarkers.Entries.Any();
public bool AimLinesVisible => CursorPath?.Alpha > 0 && CursorPath.Vertices.Count > 1;
}
}
}
17 changes: 14 additions & 3 deletions osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
Original file line number Diff line number Diff line change
@@ -1,8 +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.

#nullable disable

using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.UI;
Expand All @@ -11,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
{
public class OsuRulesetConfigManager : RulesetConfigManager<OsuRulesetSetting>
{
public OsuRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
public OsuRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant)
{
}
Expand All @@ -24,6 +22,12 @@ protected override void InitialiseDefaults()
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);

SetDefault(OsuRulesetSetting.ReplayClickMarkersEnabled, false);
SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false);
SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false);
SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false);
SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 750);
}
}

Expand All @@ -34,5 +38,12 @@ public enum OsuRulesetSetting
ShowCursorTrail,
ShowCursorRipples,
PlayfieldBorderStyle,

// Replay
ReplayClickMarkersEnabled,
ReplayFrameMarkersEnabled,
ReplayCursorPathEnabled,
ReplayCursorHideEnabled,
ReplayAnalysisDisplayLength,
}
}
29 changes: 24 additions & 5 deletions osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
Original file line number Diff line number Diff line change
@@ -1,11 +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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers;
Expand All @@ -25,18 +26,36 @@ namespace osu.Game.Rulesets.Osu.UI
{
public partial class DrawableOsuRuleset : DrawableRuleset<OsuHitObject>
{
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
bdach marked this conversation as resolved.
Show resolved Hide resolved
private Bindable<bool>? cursorHideEnabled;

public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;

public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;

public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;

public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
: base(ruleset, beatmap, mods)
{
}

public override DrawableHitObject<OsuHitObject> CreateDrawableRepresentation(OsuHitObject h) => null;
[BackgroundDependencyLoader]
private void load(ReplayPlayer? replayPlayer)
{
if (replayPlayer != null)
{
PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay));
replayPlayer.AddSettings(new ReplayAnalysisSettings(Config));

cursorHideEnabled = Config.GetBindable<bool>(OsuRulesetSetting.ReplayCursorHideEnabled);

// I have little faith in this working (other things touch cursor visibility) but haven't broken it yet.
// Let's wait for someone to report an issue before spending too much time on it.
cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true);
}
}

public override DrawableHitObject<OsuHitObject>? CreateDrawableRepresentation(OsuHitObject h) => null;

public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor

Expand Down
23 changes: 23 additions & 0 deletions osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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.Performance;
using osuTK;

namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public partial class AnalysisFrameEntry : LifetimeEntry
{
public OsuAction[] Action { get; }

public Vector2 Position { get; }

public AnalysisFrameEntry(double time, double displayLength, Vector2 position, params OsuAction[] action)
{
LifetimeStart = time;
LifetimeEnd = time + displayLength;
Position = position;
Action = action;
}
}
}
27 changes: 27 additions & 0 deletions osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Pooling;

namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public abstract partial class AnalysisMarker : PoolableDrawableWithLifetime<AnalysisFrameEntry>
{
[Resolved]
protected OsuColour Colours { get; private set; } = null!;

[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
}

protected override void OnApply(AnalysisFrameEntry entry)
{
Position = entry.Position;
}
}
}
88 changes: 88 additions & 0 deletions osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;

namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
/// <summary>
/// A marker which shows one click, with visuals focusing on the button which was clicked and the precise location of the click.
/// </summary>
public partial class ClickMarker : AnalysisMarker
{
private CircularProgress leftClickDisplay = null!;
private CircularProgress rightClickDisplay = null!;

[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.125f),
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Colour = Colours.Gray5,
},
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = Colours.Gray5,
Masking = true,
BorderThickness = 2.2f,
BorderColour = Color4.White,
Child = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0,
},
},
leftClickDisplay = new CircularProgress
{
Colour = Colours.Yellow,
Size = new Vector2(0.95f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Rotation = 180,
Progress = 0.5f,
InnerRadius = 0.18f,
RelativeSizeAxes = Axes.Both,
},
rightClickDisplay = new CircularProgress
{
Colour = Colours.Yellow,
Size = new Vector2(0.95f),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Progress = 0.5f,
InnerRadius = 0.18f,
RelativeSizeAxes = Axes.Both,
},
};

Size = new Vector2(16);
}

protected override void OnApply(AnalysisFrameEntry entry)
{
base.OnApply(entry);

leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0;
rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0;
}
}
}
20 changes: 20 additions & 0 deletions osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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.Pooling;
using osu.Game.Rulesets.Objects.Pooling;

namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer<AnalysisFrameEntry, AnalysisMarker>
{
private readonly DrawablePool<ClickMarker> clickMarkerPool;

public ClickMarkerContainer()
{
AddInternal(clickMarkerPool = new DrawablePool<ClickMarker>(30));
}

protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry));
}
}
Loading
Loading