1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 18:23:04 +08:00

Merge pull request #26607 from peppy/replay-seek-single-frame

Add ability to step forward/backwards single frames when watching replays
This commit is contained in:
Bartłomiej Dach 2024-01-22 10:36:13 +01:00 committed by GitHub
commit daa9279a23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 122 additions and 63 deletions

View File

@ -142,6 +142,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
SetDefault(OsuSetting.KeyOverlay, false); SetDefault(OsuSetting.KeyOverlay, false);
SetDefault(OsuSetting.ReplaySettingsOverlay, true); SetDefault(OsuSetting.ReplaySettingsOverlay, true);
SetDefault(OsuSetting.ReplayPlaybackControlsExpanded, true);
SetDefault(OsuSetting.GameplayLeaderboard, true); SetDefault(OsuSetting.GameplayLeaderboard, true);
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true); SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
@ -421,6 +422,7 @@ namespace osu.Game.Configuration
ProfileCoverExpanded, ProfileCoverExpanded,
EditorLimitedDistanceSnap, EditorLimitedDistanceSnap,
ReplaySettingsOverlay, ReplaySettingsOverlay,
ReplayPlaybackControlsExpanded,
AutomaticallyDownloadMissingBeatmaps, AutomaticallyDownloadMissingBeatmaps,
EditorShowSpeedChanges, EditorShowSpeedChanges,
TouchDisableGameplayTaps, TouchDisableGameplayTaps,

View File

@ -170,6 +170,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward), new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward),
new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward), new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward),
new KeyBinding(InputKey.Comma, GlobalAction.StepReplayBackward),
new KeyBinding(InputKey.Period, GlobalAction.StepReplayForward),
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings),
}; };
@ -411,7 +413,13 @@ namespace osu.Game.Input.Bindings
IncreaseOffset, IncreaseOffset,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))]
DecreaseOffset DecreaseOffset,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayForward))]
StepReplayForward,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))]
StepReplayBackward,
} }
public enum GlobalActionCategory public enum GlobalActionCategory

View File

@ -324,6 +324,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString SeekReplayBackward => new TranslatableString(getKey(@"seek_replay_backward"), @"Seek replay backward"); public static LocalisableString SeekReplayBackward => new TranslatableString(getKey(@"seek_replay_backward"), @"Seek replay backward");
/// <summary>
/// "Seek replay forward one frame"
/// </summary>
public static LocalisableString StepReplayForward => new TranslatableString(getKey(@"step_replay_forward"), @"Seek replay forward one frame");
/// <summary>
/// "Step replay backward one frame"
/// </summary>
public static LocalisableString StepReplayBackward => new TranslatableString(getKey(@"step_replay_backward"), @"Step replay backward one frame");
/// <summary> /// <summary>
/// "Toggle chat focus" /// "Toggle chat focus"
/// </summary> /// </summary>

View File

@ -9,6 +9,16 @@ namespace osu.Game.Localisation
{ {
private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings"; private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings";
/// <summary>
/// "Step backward one frame"
/// </summary>
public static LocalisableString StepBackward => new TranslatableString(getKey(@"step_backward_frame"), @"Step backward one frame");
/// <summary>
/// "Step forward one frame"
/// </summary>
public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame");
/// <summary> /// <summary>
/// "Seek backward {0} seconds" /// "Seek backward {0} seconds"
/// </summary> /// </summary>

View File

@ -1,19 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
using osuTK; using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Screens.Play.PlayerSettings namespace osu.Game.Screens.Play.PlayerSettings
{ {
@ -28,26 +26,28 @@ namespace osu.Game.Screens.Play.PlayerSettings
Precision = 0.01, Precision = 0.01,
}; };
private readonly PlayerSliderBar<double> rateSlider; private PlayerSliderBar<double> rateSlider = null!;
private readonly OsuSpriteText multiplierText; private OsuSpriteText multiplierText = null!;
private readonly BindableBool isPaused = new BindableBool(); private readonly IBindable<bool> isPaused = new BindableBool();
[Resolved] [Resolved]
private GameplayClockContainer? gameplayClock { get; set; } private ReplayPlayer replayPlayer { get; set; } = null!;
[Resolved] [Resolved]
private GameplayState? gameplayState { get; set; } private GameplayClockContainer gameplayClock { get; set; } = null!;
private IconButton pausePlay = null!;
public PlaybackSettings() public PlaybackSettings()
: base("playback") : base("playback")
{ {
const double seek_amount = 5000; }
const double seek_fast_amount = 10000;
IconButton play;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new FillFlowContainer
@ -71,50 +71,62 @@ namespace osu.Game.Screens.Play.PlayerSettings
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Icon = FontAwesome.Solid.FastBackward, Icon = FontAwesome.Solid.FastBackward,
Action = () => seek(-1, seek_fast_amount), Action = () => replayPlayer.SeekInDirection(-10),
TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_fast_amount / 1000), TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
}, },
new SeekButton new SeekButton
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Backward, Icon = FontAwesome.Solid.Backward,
Action = () => seek(-1, seek_amount), Action = () => replayPlayer.SeekInDirection(-1),
TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000), TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
}, },
play = new IconButton new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.StepBackward,
Action = () => replayPlayer.StepFrame(-1),
TooltipText = PlayerSettingsOverlayStrings.StepBackward,
},
pausePlay = new IconButton
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Scale = new Vector2(1.4f), Scale = new Vector2(1.4f),
IconScale = new Vector2(1.4f), IconScale = new Vector2(1.4f),
Icon = FontAwesome.Regular.PlayCircle,
Action = () => Action = () =>
{ {
if (gameplayClock != null) if (gameplayClock.IsRunning)
{ gameplayClock.Stop();
if (gameplayClock.IsRunning) else
gameplayClock.Stop(); gameplayClock.Start();
else
gameplayClock.Start();
}
}, },
}, },
new SeekButton new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.StepForward,
Action = () => replayPlayer.StepFrame(1),
TooltipText = PlayerSettingsOverlayStrings.StepForward,
},
new SeekButton
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Forward, Icon = FontAwesome.Solid.Forward,
Action = () => seek(1, seek_amount), Action = () => replayPlayer.SeekInDirection(1),
TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_amount / 1000), TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
}, },
new SeekButton new SeekButton
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Icon = FontAwesome.Solid.FastForward, Icon = FontAwesome.Solid.FastForward,
Action = () => seek(1, seek_fast_amount), Action = () => replayPlayer.SeekInDirection(10),
TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_fast_amount / 1000), TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
}, },
}, },
}, },
@ -141,26 +153,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
}, },
}, },
}; };
isPaused.BindValueChanged(paused =>
{
if (!paused.NewValue)
{
play.TooltipText = ToastStrings.PauseTrack;
play.Icon = FontAwesome.Regular.PauseCircle;
}
else
{
play.TooltipText = ToastStrings.PlayTrack;
play.Icon = FontAwesome.Regular.PlayCircle;
}
}, true);
void seek(int direction, double amount)
{
double target = Math.Clamp((gameplayClock?.CurrentTime ?? 0) + (direction * amount), 0, gameplayState?.Beatmap.GetLastObjectTime() ?? 0);
gameplayClock?.Seek(target);
}
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -168,8 +160,20 @@ namespace osu.Game.Screens.Play.PlayerSettings
base.LoadComplete(); base.LoadComplete();
rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true); rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true);
if (gameplayClock != null) isPaused.BindTo(gameplayClock.IsPaused);
isPaused.BindTarget = gameplayClock.IsPaused; isPaused.BindValueChanged(paused =>
{
if (!paused.NewValue)
{
pausePlay.TooltipText = ToastStrings.PauseTrack;
pausePlay.Icon = FontAwesome.Regular.PauseCircle;
}
else
{
pausePlay.TooltipText = ToastStrings.PlayTrack;
pausePlay.Icon = FontAwesome.Regular.PlayCircle;
}
}, true);
} }
private partial class SeekButton : IconButton private partial class SeekButton : IconButton

View File

@ -12,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -22,8 +23,11 @@ using osu.Game.Users;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
[Cached]
public partial class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction> public partial class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction>
{ {
public const double BASE_SEEK_AMOUNT = 1000;
private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore; private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore;
private readonly bool replayIsFailedScore; private readonly bool replayIsFailedScore;
@ -52,7 +56,7 @@ namespace osu.Game.Screens.Play
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuConfigManager config)
{ {
if (!LoadedBeatmapSuccessfully) if (!LoadedBeatmapSuccessfully)
return; return;
@ -60,7 +64,7 @@ namespace osu.Game.Screens.Play
var playbackSettings = new PlaybackSettings var playbackSettings = new PlaybackSettings
{ {
Depth = float.MaxValue, Depth = float.MaxValue,
Expanded = { Value = false } Expanded = { BindTarget = config.GetBindable<bool>(OsuSetting.ReplayPlaybackControlsExpanded) }
}; };
if (GameplayClockContainer is MasterGameplayClockContainer master) if (GameplayClockContainer is MasterGameplayClockContainer master)
@ -92,16 +96,22 @@ namespace osu.Game.Screens.Play
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
const double keyboard_seek_amount = 5000;
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.StepReplayBackward:
StepFrame(-1);
return true;
case GlobalAction.StepReplayForward:
StepFrame(1);
return true;
case GlobalAction.SeekReplayBackward: case GlobalAction.SeekReplayBackward:
keyboardSeek(-1); SeekInDirection(-1);
return true; return true;
case GlobalAction.SeekReplayForward: case GlobalAction.SeekReplayForward:
keyboardSeek(1); SeekInDirection(1);
return true; return true;
case GlobalAction.TogglePauseReplay: case GlobalAction.TogglePauseReplay:
@ -113,13 +123,28 @@ namespace osu.Game.Screens.Play
} }
return false; return false;
}
void keyboardSeek(int direction) public void StepFrame(int direction)
{ {
double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.GetLastObjectTime()); GameplayClockContainer.Stop();
Seek(target); var frames = GameplayState.Score.Replay.Frames;
}
if (frames.Count == 0)
return;
GameplayClockContainer.Seek(direction < 0
? (frames.LastOrDefault(f => f.Time < GameplayClockContainer.CurrentTime) ?? frames.First()).Time
: (frames.FirstOrDefault(f => f.Time > GameplayClockContainer.CurrentTime) ?? frames.Last()).Time
);
}
public void SeekInDirection(float amount)
{
double target = Math.Clamp(GameplayClockContainer.CurrentTime + amount * BASE_SEEK_AMOUNT, 0, GameplayState.Beatmap.GetLastObjectTime());
Seek(target);
} }
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)