1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 15:22:55 +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.KeyOverlay, false);
SetDefault(OsuSetting.ReplaySettingsOverlay, true);
SetDefault(OsuSetting.ReplayPlaybackControlsExpanded, true);
SetDefault(OsuSetting.GameplayLeaderboard, true);
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
@ -421,6 +422,7 @@ namespace osu.Game.Configuration
ProfileCoverExpanded,
EditorLimitedDistanceSnap,
ReplaySettingsOverlay,
ReplayPlaybackControlsExpanded,
AutomaticallyDownloadMissingBeatmaps,
EditorShowSpeedChanges,
TouchDisableGameplayTaps,

View File

@ -170,6 +170,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward),
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),
};
@ -411,7 +413,13 @@ namespace osu.Game.Input.Bindings
IncreaseOffset,
[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

View File

@ -324,6 +324,16 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Toggle chat focus"
/// </summary>

View File

@ -9,6 +9,16 @@ namespace osu.Game.Localisation
{
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>
/// "Seek backward {0} seconds"
/// </summary>

View File

@ -1,19 +1,17 @@
// 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.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Screens.Edit.Timing;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Screens.Play.PlayerSettings
{
@ -28,26 +26,28 @@ namespace osu.Game.Screens.Play.PlayerSettings
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]
private GameplayClockContainer? gameplayClock { get; set; }
private ReplayPlayer replayPlayer { get; set; } = null!;
[Resolved]
private GameplayState? gameplayState { get; set; }
private GameplayClockContainer gameplayClock { get; set; } = null!;
private IconButton pausePlay = null!;
public PlaybackSettings()
: base("playback")
{
const double seek_amount = 5000;
const double seek_fast_amount = 10000;
IconButton play;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new FillFlowContainer
@ -71,50 +71,62 @@ namespace osu.Game.Screens.Play.PlayerSettings
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.FastBackward,
Action = () => seek(-1, seek_fast_amount),
TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_fast_amount / 1000),
Action = () => replayPlayer.SeekInDirection(-10),
TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
},
new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Backward,
Action = () => seek(-1, seek_amount),
TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000),
Action = () => replayPlayer.SeekInDirection(-1),
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,
Origin = Anchor.Centre,
Scale = new Vector2(1.4f),
IconScale = new Vector2(1.4f),
Icon = FontAwesome.Regular.PlayCircle,
Action = () =>
{
if (gameplayClock != null)
{
if (gameplayClock.IsRunning)
gameplayClock.Stop();
else
gameplayClock.Start();
}
if (gameplayClock.IsRunning)
gameplayClock.Stop();
else
gameplayClock.Start();
},
},
new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.StepForward,
Action = () => replayPlayer.StepFrame(1),
TooltipText = PlayerSettingsOverlayStrings.StepForward,
},
new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Forward,
Action = () => seek(1, seek_amount),
TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_amount / 1000),
Action = () => replayPlayer.SeekInDirection(1),
TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
},
new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.FastForward,
Action = () => seek(1, seek_fast_amount),
TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_fast_amount / 1000),
Action = () => replayPlayer.SeekInDirection(10),
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()
@ -168,8 +160,20 @@ namespace osu.Game.Screens.Play.PlayerSettings
base.LoadComplete();
rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true);
if (gameplayClock != null)
isPaused.BindTarget = gameplayClock.IsPaused;
isPaused.BindTo(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

View File

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