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

Reimplement missing gameplay test hotkeys from stable #28705

Merged
merged 12 commits into from
Jul 4, 2024
111 changes: 111 additions & 0 deletions osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
Expand Down Expand Up @@ -224,6 +225,116 @@ public void TestClockTimeTransferIsOneDirectional()
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}

[Test]
public void TestAutoplayToggle()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();

InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});

EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Null);
AddStep("press Tab", () => InputManager.Key(Key.Tab));
AddUntilStep("replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Not.Null);
AddStep("press Tab", () => InputManager.Key(Key.Tab));
AddUntilStep("no replay active", () => editorPlayer.ChildrenOfType<DrawableRuleset>().Single().ReplayScore, () => Is.Null);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}

[Test]
public void TestQuickPause()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();

InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});

EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("clock running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.False);
AddStep("press Ctrl-P", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.P);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("clock not running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.True);
AddStep("press Ctrl-P", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.P);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("clock running", () => editorPlayer.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value, () => Is.False);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}

[Test]
public void TestQuickExitAtInitialPosition()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();

InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});

EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);

GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);

AddWaitStep("wait some", 5);

AddStep("exit player", () => InputManager.PressKey(Key.F1));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time reverted to 00:01:00", () => EditorClock.CurrentTime, () => Is.EqualTo(60_000));
}

[Test]
public void TestQuickExitAtCurrentPosition()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();

InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});

EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);

GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
// when the gameplay test is entered, the clock is expected to continue from where it was in the main editor...
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);

AddWaitStep("wait some", 5);

AddStep("exit player", () => InputManager.PressKey(Key.F2));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time moved forward", () => EditorClock.CurrentTime, () => Is.GreaterThan(60_000));
}

public override void TearDownSteps()
{
base.TearDownSteps();
Expand Down
29 changes: 27 additions & 2 deletions osu.Game/Input/Bindings/GlobalActionContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public GlobalActionContainer(OsuGameBase? game)
/// </remarks>
public override IEnumerable<IKeyBinding> DefaultKeyBindings => globalKeyBindings
.Concat(editorKeyBindings)
.Concat(editorTestPlayKeyBindings)
.Concat(inGameKeyBindings)
.Concat(replayKeyBindings)
.Concat(songSelectKeyBindings)
Expand Down Expand Up @@ -68,6 +69,9 @@ public static IEnumerable<KeyBinding> GetDefaultBindingsFor(GlobalActionCategory
case GlobalActionCategory.Overlays:
return overlayKeyBindings;

case GlobalActionCategory.EditorTestPlay:
return editorTestPlayKeyBindings;

default:
throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}");
}
Expand Down Expand Up @@ -100,7 +104,6 @@ public static IEnumerable<GlobalAction> GetGlobalActionsFor(GlobalActionCategory
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is moved/deprioritised to the overlay toggle section because in its previous place it was blocking the quick pause hotkey.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checks out since it's an overlay toggle key binding in itself.


new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),

Expand All @@ -118,6 +121,7 @@ public static IEnumerable<GlobalAction> GetGlobalActionsFor(GlobalActionCategory
new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.ToggleProfile),
};

private static IEnumerable<KeyBinding> editorKeyBindings => new[]
Expand Down Expand Up @@ -145,6 +149,14 @@ public static IEnumerable<GlobalAction> GetGlobalActionsFor(GlobalActionCategory
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
};

private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.Tab }, GlobalAction.EditorTestPlayToggleAutoplay),
new KeyBinding(new[] { InputKey.Control, InputKey.P }, GlobalAction.EditorTestPlayToggleQuickPause),
new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorTestPlayQuickExitToInitialTime),
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorTestPlayQuickExitToCurrentTime),
};

private static IEnumerable<KeyBinding> inGameKeyBindings => new[]
{
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
Expand Down Expand Up @@ -432,6 +444,18 @@ public enum GlobalAction

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))]
EditorToggleScaleControl,

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleAutoplay))]
EditorTestPlayToggleAutoplay,

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayToggleQuickPause))]
EditorTestPlayToggleQuickPause,

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToInitialTime))]
EditorTestPlayQuickExitToInitialTime,

[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToCurrentTime))]
EditorTestPlayQuickExitToCurrentTime,
}

public enum GlobalActionCategory
Expand All @@ -442,6 +466,7 @@ public enum GlobalActionCategory
Replay,
SongSelect,
AudioControl,
Overlays
Overlays,
EditorTestPlay,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These getting a separate section will likely look gratuitous but it bypasses several issues:

  • If they were to be implemented as non-rebindable, they wouldn't be listed anywhere, so there'd probably need to be text that explains that these exist
  • F1 and F2 are already used in the editor section (for compose/design tabs), and keybindings must be unique within a section
  • This also solves various input priority weirdness. As it stands, KeyBindingContainer takes precedence over plain OnKeyDown() which meant that other various bindings like leaderboard toggle were eating these hotkeys when implementing them via OnKeyDown().

}
}
20 changes: 20 additions & 0 deletions osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,26 @@ public static class GlobalActionKeyBindingStrings
/// </summary>
public static LocalisableString EditorToggleScaleControl => new TranslatableString(getKey(@"editor_toggle_scale_control"), @"Toggle scale control");

/// <summary>
/// "Toggle autoplay"
/// </summary>
public static LocalisableString EditorTestPlayToggleAutoplay => new TranslatableString(getKey(@"editor_test_play_toggle_autoplay"), @"Toggle autoplay");

/// <summary>
/// "Toggle quick pause"
/// </summary>
public static LocalisableString EditorTestPlayToggleQuickPause => new TranslatableString(getKey(@"editor_test_play_toggle_quick_pause"), @"Toggle quick pause");

/// <summary>
/// "Quick exit to initial time"
/// </summary>
public static LocalisableString EditorTestPlayQuickExitToInitialTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_initial_time"), @"Quick exit to initial time");

/// <summary>
/// "Quick exit to current time"
/// </summary>
public static LocalisableString EditorTestPlayQuickExitToCurrentTime => new TranslatableString(getKey(@"editor_test_play_quick_exit_to_current_time"), @"Quick exit to current time");

/// <summary>
/// "Increase mod speed"
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions osu.Game/Localisation/InputSettingsStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public static class InputSettingsStrings
/// </summary>
public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor");

/// <summary>
/// "Editor: Test play"
/// </summary>
public static LocalisableString EditorTestPlaySection => new TranslatableString(getKey(@"editor_test_play_section"), @"Editor: Test play");

/// <summary>
/// "Reset all bindings in section"
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ private void load()
new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame),
new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay),
new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor),
new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorTestPlaySection, GlobalActionCategory.EditorTestPlay),
});
}
}
Expand Down
76 changes: 75 additions & 1 deletion osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Play;
using osu.Game.Users;

namespace osu.Game.Screens.Edit.GameplayTest
{
public partial class EditorPlayer : Player
public partial class EditorPlayer : Player, IKeyBindingHandler<GlobalAction>
{
private readonly Editor editor;
private readonly EditorState editorState;
Expand Down Expand Up @@ -133,6 +137,76 @@ protected override void PrepareReplay()

protected override bool CheckModsAllowFailure() => false; // never fail.

public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;

switch (e.Action)
{
case GlobalAction.EditorTestPlayToggleAutoplay:
toggleAutoplay();
return true;

case GlobalAction.EditorTestPlayToggleQuickPause:
toggleQuickPause();
return true;

case GlobalAction.EditorTestPlayQuickExitToInitialTime:
quickExit(false);
return true;

case GlobalAction.EditorTestPlayQuickExitToCurrentTime:
quickExit(true);
return true;

default:
return false;
}
}

public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}

private void toggleAutoplay()
{
if (DrawableRuleset.ReplayScore == null)
{
var autoplay = Ruleset.Value.CreateInstance().GetAutoplayMod();
if (autoplay == null)
return;

var score = autoplay.CreateScoreFromReplayData(GameplayState.Beatmap, [autoplay]);
frenzibyte marked this conversation as resolved.
Show resolved Hide resolved

// remove past frames to prevent replay frame handler from seeking back to start in an attempt to play back the entirety of the replay.
score.Replay.Frames.RemoveAll(f => f.Time <= GameplayClockContainer.CurrentTime);

DrawableRuleset.SetReplayScore(score);
// Without this schedule, the `GlobalCursorDisplay.Update()` machinery will fade the gameplay cursor out, but we still want it to show.
Schedule(() => DrawableRuleset.Cursor?.Show());
}
else
DrawableRuleset.SetReplayScore(null);
}

private void toggleQuickPause()
{
if (GameplayClockContainer.IsPaused.Value)
GameplayClockContainer.Start();
else
GameplayClockContainer.Stop();
}

private void quickExit(bool useCurrentTime)
{
if (useCurrentTime)
editorState.Time = GameplayClockContainer.CurrentTime;

editor.RestoreState(editorState);
this.Exit();
}

public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
Expand Down
Loading