1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 08:52:55 +08:00

Merge branch 'master' into comment-reply

This commit is contained in:
Dean Herbert 2023-01-19 16:01:47 +09:00
commit 56ce8504ae
28 changed files with 1300 additions and 188 deletions

View File

@ -0,0 +1,405 @@
// 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.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public partial class TestSceneTouchInput : OsuManualInputManagerTestScene
{
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private TestActionKeyCounter leftKeyCounter = null!;
private TestActionKeyCounter rightKeyCounter = null!;
private OsuInputManager osuInputManager = null!;
[SetUpSteps]
public void SetUpSteps()
{
releaseAllTouches();
AddStep("Create tests", () =>
{
Children = new Drawable[]
{
osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo)
{
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton)
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
X = -100,
},
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton)
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
X = 100,
}
},
}
},
new TouchVisualiser(),
};
});
}
[Test]
public void TestSimpleInput()
{
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
// Subsequent touches should be ignored (except position).
beginTouch(TouchSource.Touch3);
checkPosition(TouchSource.Touch3);
beginTouch(TouchSource.Touch4);
checkPosition(TouchSource.Touch4);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
assertKeyCounter(1, 1);
}
[Test]
public void TestPositionalInputUpdatesOnlyFromMostRecentTouch()
{
beginTouch(TouchSource.Touch1);
checkPosition(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
checkPosition(TouchSource.Touch2);
beginTouch(TouchSource.Touch1, Vector2.One);
checkPosition(TouchSource.Touch2);
endTouch(TouchSource.Touch2);
checkPosition(TouchSource.Touch2);
// note that touch1 was never ended, but becomes active for tracking again.
beginTouch(TouchSource.Touch1);
checkPosition(TouchSource.Touch1);
}
[Test]
public void TestMovementWhileDisallowed()
{
// aka "autopilot" mod
AddStep("Disallow gameplay cursor movement", () => osuInputManager.AllowUserCursorMovement = false);
Vector2? positionBefore = null;
AddStep("Store cursor position", () => positionBefore = osuInputManager.CurrentState.Mouse.Position);
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
AddAssert("Cursor position unchanged", () => osuInputManager.CurrentState.Mouse.Position, () => Is.EqualTo(positionBefore));
}
[Test]
public void TestActionWhileDisallowed()
{
// aka "relax" mod
AddStep("Disallow gameplay actions", () => osuInputManager.AllowGameplayInputs = false);
beginTouch(TouchSource.Touch1);
assertKeyCounter(0, 0);
checkNotPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
}
[Test]
public void TestInputWhileMouseButtonsDisabled()
{
AddStep("Disable mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, true));
beginTouch(TouchSource.Touch1);
assertKeyCounter(0, 0);
checkNotPressed(OsuAction.LeftButton);
checkPosition(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
assertKeyCounter(0, 0);
checkNotPressed(OsuAction.LeftButton);
checkNotPressed(OsuAction.RightButton);
checkPosition(TouchSource.Touch2);
}
[Test]
public void TestAlternatingInput()
{
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
for (int i = 0; i < 2; i++)
{
endTouch(TouchSource.Touch1);
checkPressed(OsuAction.RightButton);
checkNotPressed(OsuAction.LeftButton);
beginTouch(TouchSource.Touch1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
endTouch(TouchSource.Touch2);
checkPressed(OsuAction.LeftButton);
checkNotPressed(OsuAction.RightButton);
beginTouch(TouchSource.Touch2);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
}
}
[Test]
public void TestPressReleaseOrder()
{
beginTouch(TouchSource.Touch1);
beginTouch(TouchSource.Touch2);
beginTouch(TouchSource.Touch3);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
// Touch 3 was ignored, but let's ensure that if 1 or 2 are released, 3 will be handled a second attempt.
endTouch(TouchSource.Touch1);
assertKeyCounter(1, 1);
checkPressed(OsuAction.RightButton);
endTouch(TouchSource.Touch3);
assertKeyCounter(1, 1);
checkPressed(OsuAction.RightButton);
beginTouch(TouchSource.Touch3);
assertKeyCounter(2, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
}
[Test]
public void TestWithDisallowedUserCursor()
{
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
checkPressed(OsuAction.LeftButton);
beginTouch(TouchSource.Touch2);
assertKeyCounter(1, 1);
checkPressed(OsuAction.RightButton);
// Subsequent touches should be ignored.
beginTouch(TouchSource.Touch3);
beginTouch(TouchSource.Touch4);
assertKeyCounter(1, 1);
checkPressed(OsuAction.LeftButton);
checkPressed(OsuAction.RightButton);
assertKeyCounter(1, 1);
}
private void beginTouch(TouchSource source, Vector2? screenSpacePosition = null) =>
AddStep($"Begin touch for {source}", () => InputManager.BeginTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source))));
private void endTouch(TouchSource source, Vector2? screenSpacePosition = null) =>
AddStep($"Release touch for {source}", () => InputManager.EndTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source))));
private Vector2 getSanePositionForSource(TouchSource source)
{
return new Vector2(
osuInputManager.ScreenSpaceDrawQuad.Centre.X + osuInputManager.ScreenSpaceDrawQuad.Width * (-1 + (int)source) / 8,
osuInputManager.ScreenSpaceDrawQuad.Centre.Y - 100
);
}
private void checkPosition(TouchSource touchSource) =>
AddAssert("Cursor position is correct", () => osuInputManager.CurrentState.Mouse.Position, () => Is.EqualTo(getSanePositionForSource(touchSource)));
private void assertKeyCounter(int left, int right)
{
AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses, () => Is.EqualTo(left));
AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses, () => Is.EqualTo(right));
}
private void releaseAllTouches()
{
AddStep("Release all touches", () =>
{
config.SetValue(OsuSetting.MouseDisableButtons, false);
foreach (TouchSource source in InputManager.CurrentState.Touch.ActiveSources)
InputManager.EndTouch(new Touch(source, osuInputManager.ScreenSpaceDrawQuad.Centre));
});
}
private void checkNotPressed(OsuAction action) => AddAssert($"Not pressing {action}", () => !osuInputManager.PressedActions.Contains(action));
private void checkPressed(OsuAction action) => AddAssert($"Is pressing {action}", () => osuInputManager.PressedActions.Contains(action));
public partial class TestActionKeyCounter : KeyCounter, IKeyBindingHandler<OsuAction>
{
public OsuAction Action { get; }
public TestActionKeyCounter(OsuAction action)
: base(action.ToString())
{
Action = action;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
if (e.Action == Action)
{
IsLit = true;
Increment();
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
if (e.Action == Action) IsLit = false;
}
}
public partial class TouchVisualiser : CompositeDrawable
{
private readonly Drawable?[] drawableTouches = new Drawable?[TouchState.MAX_TOUCH_COUNT];
public TouchVisualiser()
{
RelativeSizeAxes = Axes.Both;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool OnTouchDown(TouchDownEvent e)
{
if (IsDisposed)
return false;
var circle = new Circle
{
Alpha = 0.5f,
Origin = Anchor.Centre,
Size = new Vector2(20),
Position = e.Touch.Position,
Colour = colourFor(e.Touch.Source),
};
AddInternal(circle);
drawableTouches[(int)e.Touch.Source] = circle;
return false;
}
protected override void OnTouchMove(TouchMoveEvent e)
{
if (IsDisposed)
return;
var circle = drawableTouches[(int)e.Touch.Source];
Debug.Assert(circle != null);
AddInternal(new FadingCircle(circle));
circle.Position = e.Touch.Position;
}
protected override void OnTouchUp(TouchUpEvent e)
{
var circle = drawableTouches[(int)e.Touch.Source];
Debug.Assert(circle != null);
circle.FadeOut(200, Easing.OutQuint).Expire();
drawableTouches[(int)e.Touch.Source] = null;
}
private Color4 colourFor(TouchSource source)
{
return Color4.FromHsv(new Vector4((float)source / TouchState.MAX_TOUCH_COUNT, 1f, 1f, 1f));
}
private partial class FadingCircle : Circle
{
public FadingCircle(Drawable source)
{
Origin = Anchor.Centre;
Size = source.Size;
Position = source.Position;
Colour = source.Colour;
}
protected override void LoadComplete()
{
base.LoadComplete();
this.FadeOut(200).Expire();
}
}
}
}
}

View File

@ -1,16 +1,15 @@
// 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.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using osu.Framework.Input;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@ -28,6 +27,7 @@ namespace osu.Game.Rulesets.Osu
/// </remarks>
public bool AllowGameplayInputs
{
get => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs;
set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value;
}
@ -45,6 +45,12 @@ namespace osu.Game.Rulesets.Osu
{
}
[BackgroundDependencyLoader]
private void load()
{
Add(new OsuTouchInputMapper(this) { RelativeSizeAxes = Axes.Both });
}
protected override bool Handle(UIEvent e)
{
if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
@ -52,19 +58,6 @@ namespace osu.Game.Rulesets.Osu
return base.Handle(e);
}
protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
{
if (!AllowUserCursorMovement)
{
// Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse.
// Primarily relied upon by the "autopilot" osu! mod.
var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position);
e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null);
}
return base.HandleMouseTouchStateChange(e);
}
private partial class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
private bool allowGameplayInputs = true;

View File

@ -0,0 +1,104 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.UI
{
public partial class OsuTouchInputMapper : Drawable
{
/// <summary>
/// All the active <see cref="TouchSource"/>s and the <see cref="OsuAction"/> that it triggered (if any).
/// Ordered from oldest to newest touch chronologically.
/// </summary>
private readonly List<TrackedTouch> trackedTouches = new List<TrackedTouch>();
private readonly OsuInputManager osuInputManager;
private Bindable<bool> mouseDisabled = null!;
public OsuTouchInputMapper(OsuInputManager inputManager)
{
osuInputManager = inputManager;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
// The mouse button disable setting affects touch. It's a bit weird.
// This is mostly just doing the same as what is done in RulesetInputManager to match behaviour.
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
}
protected override void OnTouchMove(TouchMoveEvent e)
{
base.OnTouchMove(e);
handleTouchMovement(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
OsuAction action = trackedTouches.Any(t => t.Action == OsuAction.LeftButton)
? OsuAction.RightButton
: OsuAction.LeftButton;
// Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future.
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action);
trackedTouches.Add(new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null));
// Important to update position before triggering the pressed action.
handleTouchMovement(e);
if (shouldResultInAction)
osuInputManager.KeyBindingContainer.TriggerPressed(action);
return true;
}
private void handleTouchMovement(TouchEvent touchEvent)
{
// Movement should only be tracked for the most recent touch.
if (touchEvent.Touch.Source != trackedTouches.Last().Source)
return;
if (!osuInputManager.AllowUserCursorMovement)
return;
new MousePositionAbsoluteInput { Position = touchEvent.ScreenSpaceTouch.Position }.Apply(osuInputManager.CurrentState, osuInputManager);
}
protected override void OnTouchUp(TouchUpEvent e)
{
var tracked = trackedTouches.Single(t => t.Source == e.Touch.Source);
if (tracked.Action is OsuAction action)
osuInputManager.KeyBindingContainer.TriggerReleased(action);
trackedTouches.Remove(tracked);
base.OnTouchUp(e);
}
private class TrackedTouch
{
public readonly TouchSource Source;
public readonly OsuAction? Action;
public TrackedTouch(TouchSource source, OsuAction? action)
{
Source = source;
Action = action;
}
}
}
}

View File

@ -41,6 +41,8 @@ namespace osu.Game.Tests.Skins
"Archives/modified-default-20220818.osk",
// Covers longest combo counter
"Archives/modified-default-20221012.osk",
// Covers Argon variant of song progress bar
"Archives/modified-argon-20221024.osk",
// Covers TextElement and BeatmapInfoDrawable
"Archives/modified-default-20221102.osk",
// Covers BPM counter.

View File

@ -20,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var implementation = skin is LegacySkin
? CreateLegacyImplementation()
: CreateDefaultImplementation();
: skin is ArgonSkin
? CreateArgonImplementation()
: CreateDefaultImplementation();
implementation.Anchor = Anchor.Centre;
implementation.Origin = Anchor.Centre;
@ -29,6 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
});
protected abstract Drawable CreateDefaultImplementation();
protected virtual Drawable CreateArgonImplementation() => CreateDefaultImplementation();
protected abstract Drawable CreateLegacyImplementation();
}
}

View File

@ -14,7 +14,7 @@ using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Gameplay
{
[TestFixture]
public partial class TestSceneSongProgressGraph : OsuTestScene
public partial class TestSceneDefaultSongProgressGraph : OsuTestScene
{
private TestSongProgressGraph graph;
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
graph.Objects = objects;
}
private partial class TestSongProgressGraph : SongProgressGraph
private partial class TestSongProgressGraph : DefaultSongProgressGraph
{
public int CreationCount { get; private set; }

View File

@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestInputDoesntWorkWhenHUDHidden()
{
SongProgressBar? getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().SingleOrDefault();
ArgonSongProgress? getSongProgress() => hudOverlay.ChildrenOfType<ArgonSongProgress>().SingleOrDefault();
bool seeked = false;
@ -204,8 +204,8 @@ namespace osu.Game.Tests.Visual.Gameplay
Debug.Assert(progress != null);
progress.ShowHandle = true;
progress.OnSeek += _ => seeked = true;
progress.Interactive.Value = true;
progress.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += _ => seeked = true;
});
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);

View File

@ -25,35 +25,33 @@ namespace osu.Game.Tests.Visual.Gameplay
private JudgementTally judgementTally = null!;
private TestJudgementCounterDisplay counterDisplay = null!;
private DependencyProvidingContainer content = null!;
protected override Container<Drawable> Content => content;
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
private int iteration;
[SetUpSteps]
public void SetupSteps() => AddStep("Create components", () =>
public void SetUpSteps() => AddStep("Create components", () =>
{
var ruleset = CreateRuleset();
Debug.Assert(ruleset != null);
scoreProcessor = new ScoreProcessor(ruleset);
Child = new DependencyProvidingContainer
base.Content.Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) },
Children = new Drawable[]
{
judgementTally = new JudgementTally(),
new DependencyProvidingContainer
content = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) },
Child = counterDisplay = new TestJudgementCounterDisplay
{
Margin = new MarginPadding { Top = 100 },
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
}
}
},
};
@ -78,6 +76,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestAddJudgementsToCounters()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Great), 2);
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Miss), 2);
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Meh), 2);
@ -86,6 +86,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestAddWhilstHidden()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2);
AddAssert("Check value added whilst hidden", () => hiddenCount() == 2);
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
@ -94,6 +96,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestChangeFlowDirection()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical);
AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal);
}
@ -101,6 +105,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestToggleJudgementNames()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = false);
AddWaitStep("wait some", 2);
AddAssert("Assert hidden", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 0);
@ -112,15 +118,29 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestHideMaxValue()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false);
AddWaitStep("wait some", 2);
AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
AddStep("Show max judgement", () => counterDisplay.ShowMaxJudgement.Value = true);
}
[Test]
public void TestMaxValueStartsHidden()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay
{
ShowMaxJudgement = { Value = false }
});
AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
}
[Test]
public void TestCycleDisplayModes()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple);
AddWaitStep("wait some", 2);
AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 0);
@ -139,6 +159,13 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestJudgementCounterDisplay : JudgementCounterDisplay
{
public new FillFlowContainer<JudgementCounter> CounterFlow => base.CounterFlow;
public TestJudgementCounterDisplay()
{
Margin = new MarginPadding { Top = 100 };
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
}
}
}
}

View File

@ -2,14 +2,14 @@
// 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.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
@ -28,50 +28,62 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time));
FrameStabilityContainer frameStabilityContainer;
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
{
Child = frameStabilityContainer = new FrameStabilityContainer
{
MaxCatchUpFrames = 1
}
});
Dependencies.CacheAs<IGameplayClock>(gameplayClockContainer);
Dependencies.CacheAs<IFrameStableClock>(frameStabilityContainer);
}
[SetUpSteps]
public void SetupSteps()
{
AddStep("reset clock", () => gameplayClockContainer.Reset());
AddStep("set hit objects", setHitObjects);
AddStep("set hit objects", () => this.ChildrenOfType<SongProgress>().ForEach(progress => progress.Objects = Beatmap.Value.Beatmap.HitObjects));
AddStep("hook seeking", () =>
{
applyToDefaultProgress(d => d.ChildrenOfType<DefaultSongProgressBar>().Single().OnSeek += t => gameplayClockContainer.Seek(t));
applyToArgonProgress(d => d.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += t => gameplayClockContainer.Seek(t));
});
AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time));
AddStep("start", () => gameplayClockContainer.Start());
}
[Test]
public void TestDisplay()
public void TestBasic()
{
AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time));
AddStep("start", gameplayClockContainer.Start);
AddToggleStep("toggle seeking", b =>
{
applyToDefaultProgress(s => s.Interactive.Value = b);
applyToArgonProgress(s => s.Interactive.Value = b);
});
AddToggleStep("toggle graph", b =>
{
applyToDefaultProgress(s => s.ShowGraph.Value = b);
applyToArgonProgress(s => s.ShowGraph.Value = b);
});
AddStep("stop", gameplayClockContainer.Stop);
}
[Test]
public void TestToggleSeeking()
{
void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
this.ChildrenOfType<DefaultSongProgress>().ForEach(action);
private void applyToArgonProgress(Action<ArgonSongProgress> action) =>
this.ChildrenOfType<ArgonSongProgress>().ForEach(action);
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
AddStep("hide graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = false));
AddStep("disallow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = false));
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
AddStep("show graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = true));
}
private void setHitObjects()
{
var objects = new List<HitObject>();
for (double i = 0; i < 5000; i++)
objects.Add(new HitObject { StartTime = i });
this.ChildrenOfType<SongProgress>().ForEach(progress => progress.Objects = objects);
}
private void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
this.ChildrenOfType<DefaultSongProgress>().ForEach(action);
protected override Drawable CreateDefaultImplementation() => new DefaultSongProgress();
protected override Drawable CreateArgonImplementation() => new ArgonSongProgress();
protected override Drawable CreateLegacyImplementation() => new LegacySongProgress();
}
}

View File

@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
!p.ChildrenOfType<HoldForMenuButton>().Any() &&
p.ChildrenOfType<SongProgressBar>().SingleOrDefault()?.ShowHandle == false));
p.ChildrenOfType<ArgonSongProgressBar>().SingleOrDefault()?.Interactive == false));
AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
}

View File

@ -580,10 +580,9 @@ namespace osu.Game.Tests.Visual.SongSelect
/// Ensures stability is maintained on different sort modes for items with equal properties.
/// </summary>
[Test]
public void TestSortingStability()
public void TestSortingStabilityDateAdded()
{
var sets = new List<BeatmapSetInfo>();
int idOffset = 0;
AddStep("Populuate beatmap sets", () =>
{
@ -593,38 +592,34 @@ namespace osu.Game.Tests.Visual.SongSelect
{
var set = TestResources.CreateTestBeatmapSetInfo();
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(i);
// only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = $"artist {i / 2}";
beatmap.Metadata.Title = $"title {9 - i}";
beatmap.Metadata.Artist = "a";
beatmap.Metadata.Title = "b";
sets.Add(set);
}
idOffset = sets.First().OnlineID;
});
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddAssert("Items are in reverse order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + sets.Count - index - 1).All(b => b));
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items reset to original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
}
/// <summary>
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
/// </summary>
[Test]
public void TestSortingStabilityWithNewItems()
public void TestSortingStabilityWithRemovedAndReaddedItem()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
int idOffset = 0;
AddStep("Populuate beatmap sets", () =>
{
@ -640,16 +635,68 @@ namespace osu.Game.Tests.Visual.SongSelect
beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title";
// testing the case where DateAdded happens to equal (quite rare).
set.DateAdded = DateTimeOffset.UnixEpoch;
sets.Add(set);
}
idOffset = sets.First().OnlineID;
});
Guid[] originalOrder = null!;
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
assertOriginalOrderMaintained();
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
AddStep("Remove item", () => carousel.RemoveBeatmapSet(sets[1]));
AddStep("Re-add item", () => carousel.UpdateBeatmapSet(sets[1]));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
}
/// <summary>
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
/// </summary>
[Test]
public void TestSortingStabilityWithNewItems()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
AddStep("Populuate beatmap sets", () =>
{
sets.Clear();
for (int i = 0; i < 3; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(3);
// only need to set the first as they are a shared reference.
var beatmap = set.Beatmaps.First();
beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title";
// testing the case where DateAdded happens to equal (quite rare).
set.DateAdded = DateTimeOffset.UnixEpoch;
sets.Add(set);
}
});
Guid[] originalOrder = null!;
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
AddStep("Add new item", () =>
{
@ -661,19 +708,18 @@ namespace osu.Game.Tests.Visual.SongSelect
beatmap.Metadata.Artist = "same artist";
beatmap.Metadata.Title = "same title";
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(1);
carousel.UpdateBeatmapSet(set);
// add set to expected ordering
originalOrder = originalOrder.Prepend(set.ID).ToArray();
});
assertOriginalOrderMaintained();
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
assertOriginalOrderMaintained();
void assertOriginalOrderMaintained()
{
AddAssert("Items remain in original order",
() => carousel.BeatmapSets.Select(s => s.OnlineID), () => Is.EqualTo(carousel.BeatmapSets.Select((set, index) => idOffset + index)));
}
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
}
[Test]

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("beatmap density with granularity of 200", () => beatmapDensity());
AddStep("beatmap density with granularity of 300", () => beatmapDensity(300));
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().ToArray());
AddStep("change colour", () =>
AddStep("change tier colours", () =>
{
graph.TierColours = new[]
{
@ -62,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Colour4.Blue
};
});
AddStep("reset colour", () =>
AddStep("reset tier colours", () =>
{
graph.TierColours = new[]
{
@ -74,6 +75,12 @@ namespace osu.Game.Tests.Visual.UserInterface
Colour4.Green
};
});
AddStep("set graph colour to blue", () => graph.Colour = Colour4.Blue);
AddStep("set graph colour to transparent", () => graph.Colour = Colour4.Transparent);
AddStep("set graph colour to vertical gradient", () => graph.Colour = ColourInfo.GradientVertical(Colour4.White, Colour4.Black));
AddStep("set graph colour to horizontal gradient", () => graph.Colour = ColourInfo.GradientHorizontal(Colour4.White, Colour4.Black));
AddStep("reset graph colour", () => graph.Colour = Colour4.White);
}
private void sinFunction(int size = 100)

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders;
@ -48,17 +49,14 @@ namespace osu.Game.Graphics.UserInterface
}
}
private Colour4[] tierColours;
private IReadOnlyList<Colour4> tierColours;
public Colour4[] TierColours
public IReadOnlyList<Colour4> TierColours
{
get => tierColours;
set
{
if (value.Length == 0 || value == tierColours)
return;
tierCount = value.Length;
tierCount = value.Count;
tierColours = value;
graphNeedsUpdate = true;
@ -154,7 +152,21 @@ namespace osu.Game.Graphics.UserInterface
segments.Sort();
}
private Colour4 getTierColour(int tier) => tier >= 0 ? tierColours[tier] : new Colour4(0, 0, 0, 0);
private ColourInfo getSegmentColour(SegmentInfo segment)
{
var segmentColour = new ColourInfo
{
TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)),
TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)),
BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)),
BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f))
};
var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0);
segmentColour.ApplyChild(tierColour);
return segmentColour;
}
protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this);
@ -240,7 +252,7 @@ namespace osu.Game.Graphics.UserInterface
Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)),
Source.getTierColour(segment.Tier));
Source.getSegmentColour(segment));
}
shader.Unbind();

View File

@ -0,0 +1,117 @@
// 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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgress : SongProgress
{
private readonly SongProgressInfo info;
private readonly ArgonSongProgressGraph graph;
private readonly ArgonSongProgressBar bar;
private readonly Container graphContainer;
private const float bar_height = 10;
[SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")]
public Bindable<bool> ShowGraph { get; } = new BindableBool(true);
[Resolved]
private Player? player { get; set; }
public ArgonSongProgress()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Masking = true;
CornerRadius = 5;
Children = new Drawable[]
{
info = new SongProgressInfo
{
Origin = Anchor.TopLeft,
Name = "Info",
Anchor = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
ShowProgress = false
},
bar = new ArgonSongProgressBar(bar_height)
{
Name = "Seek bar",
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
OnSeek = time => player?.Seek(time),
},
graphContainer = new Container
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Masking = true,
CornerRadius = 5,
Child = graph = new ArgonSongProgressGraph
{
Name = "Difficulty graph",
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive
},
RelativeSizeAxes = Axes.X,
},
};
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load()
{
info.TextColour = Colour4.White;
info.Font = OsuFont.Torus.With(size: 18, weight: FontWeight.Bold);
}
protected override void LoadComplete()
{
base.LoadComplete();
Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
}
protected override void UpdateObjects(IEnumerable<HitObject> objects)
{
graph.Objects = objects;
info.StartTime = bar.StartTime = FirstHitTime;
info.EndTime = bar.EndTime = LastHitTime;
}
private void updateGraphVisibility()
{
graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In);
bar.ShowBackground = !ShowGraph.Value;
}
protected override void Update()
{
base.Update();
Height = bar.Height + bar_height + info.Height;
graphContainer.Height = bar.Height;
}
protected override void UpdateProgress(double progress, bool isIntro)
{
bar.TrackTime = GameplayClock.CurrentTime;
if (isIntro)
bar.CurrentTime = 0;
else
bar.CurrentTime = FrameStableClock.CurrentTime;
}
}
}

View File

@ -0,0 +1,266 @@
// 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.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgressBar : SliderBar<double>
{
public Action<double>? OnSeek { get; set; }
// Parent will handle restricting the area of valid input.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly float barHeight;
private readonly RoundedBar playfieldBar;
private readonly RoundedBar catchupBar;
private readonly Box background;
private readonly BindableBool showBackground = new BindableBool();
private readonly ColourInfo mainColour;
private readonly ColourInfo mainColourDarkened;
private ColourInfo catchUpColour;
private ColourInfo catchUpColourDarkened;
public bool ShowBackground
{
get => showBackground.Value;
set => showBackground.Value = value;
}
public double StartTime
{
private get => CurrentNumber.MinValue;
set => CurrentNumber.MinValue = value;
}
public double EndTime
{
private get => CurrentNumber.MaxValue;
set => CurrentNumber.MaxValue = value;
}
public double CurrentTime
{
private get => CurrentNumber.Value;
set => CurrentNumber.Value = value;
}
public double TrackTime
{
private get => currentTrackTime.Value;
set => currentTrackTime.Value = value;
}
private double length => EndTime - StartTime;
private readonly BindableNumber<double> currentTrackTime;
public bool Interactive { get; set; }
public ArgonSongProgressBar(float barHeight)
{
currentTrackTime = new BindableDouble();
setupAlternateValue();
StartTime = 0;
EndTime = 1;
RelativeSizeAxes = Axes.X;
Height = this.barHeight = barHeight;
CornerRadius = 5;
Masking = true;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = Colour4.White.Darken(1 + 1 / 4f)
},
catchupBar = new RoundedBar
{
Name = "Audio bar",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
CornerRadius = 5,
AlwaysPresent = true,
RelativeSizeAxes = Axes.Both
},
playfieldBar = new RoundedBar
{
Name = "Playfield bar",
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
CornerRadius = 5,
AccentColour = mainColour = Color4.White,
RelativeSizeAxes = Axes.Both
},
};
mainColourDarkened = Colour4.White.Darken(1 / 3f);
}
private void setupAlternateValue()
{
CurrentNumber.MaxValueChanged += v => currentTrackTime.MaxValue = v;
CurrentNumber.MinValueChanged += v => currentTrackTime.MinValue = v;
CurrentNumber.PrecisionChanged += v => currentTrackTime.Precision = v;
}
private float normalizedReference
{
get
{
if (EndTime - StartTime == 0)
return 1;
return (float)((TrackTime - StartTime) / length);
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
catchUpColour = colours.BlueLight;
catchUpColourDarkened = colours.BlueDark;
showBackground.BindValueChanged(_ => updateBackground(), true);
}
private void updateBackground()
{
background.FadeTo(showBackground.Value ? 1 / 4f : 0, 200, Easing.In);
playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), ShowBackground ? mainColour : mainColourDarkened, 200, Easing.In);
}
protected override bool OnHover(HoverEvent e)
{
if (Interactive)
this.ResizeHeightTo(barHeight * 3.5f, 200, Easing.Out);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
this.ResizeHeightTo(barHeight, 800, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override void UpdateValue(float value)
{
// Handled in Update
}
protected override void Update()
{
base.Update();
playfieldBar.Length = (float)Interpolation.Lerp(playfieldBar.Length, NormalizedValue, Math.Clamp(Time.Elapsed / 40, 0, 1));
catchupBar.Length = (float)Interpolation.Lerp(catchupBar.Length, normalizedReference, Math.Clamp(Time.Elapsed / 40, 0, 1));
if (TrackTime < CurrentTime)
ChangeChildDepth(catchupBar, -1);
else
ChangeChildDepth(catchupBar, 0);
float timeDelta = (float)(Math.Abs(CurrentTime - TrackTime));
const float colour_transition_threshold = 20000;
catchupBar.AccentColour = Interpolation.ValueAt(
Math.Min(timeDelta, colour_transition_threshold),
ShowBackground ? mainColour : mainColourDarkened,
ShowBackground ? catchUpColour : catchUpColourDarkened,
0, colour_transition_threshold,
Easing.OutQuint);
catchupBar.Alpha = Math.Max(1, catchupBar.Length);
}
private ScheduledDelegate? scheduledSeek;
protected override void OnUserChange(double value)
{
scheduledSeek?.Cancel();
scheduledSeek = Schedule(() =>
{
if (Interactive)
OnSeek?.Invoke(value);
});
}
private partial class RoundedBar : Container
{
private readonly Box fill;
private readonly Container mask;
private float length;
public RoundedBar()
{
Masking = true;
Children = new[]
{
mask = new Container
{
Masking = true,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(1),
Child = fill = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.White
}
}
};
}
public float Length
{
get => length;
set
{
length = value;
mask.Width = value * DrawWidth;
fill.Width = value * DrawWidth;
}
}
public new float CornerRadius
{
get => base.CornerRadius;
set
{
base.CornerRadius = value;
mask.CornerRadius = value;
}
}
public ColourInfo AccentColour
{
get => fill.Colour;
set => fill.Colour = value;
}
}
}
}

View File

@ -0,0 +1,64 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgressGraph : SegmentedGraph<int>
{
private IEnumerable<HitObject>? objects;
public IEnumerable<HitObject> Objects
{
set
{
objects = value;
const int granularity = 200;
int[] values = new int[granularity];
if (!objects.Any())
return;
double firstHit = objects.First().StartTime;
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0)
lastHit = objects.Last().StartTime;
double interval = (lastHit - firstHit + 1) / granularity;
foreach (var h in objects)
{
double endTime = h.GetEndTime();
Debug.Assert(endTime >= h.StartTime);
int startRange = (int)((h.StartTime - firstHit) / interval);
int endRange = (int)((endTime - firstHit) / interval);
for (int i = startRange; i <= endRange; i++)
values[i]++;
}
Values = values;
}
}
public ArgonSongProgressGraph()
: base(5)
{
var colours = new List<Colour4>();
for (int i = 0; i < 5; i++)
colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f));
TierColours = colours;
}
}
}

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Screens.Play.HUD
@ -23,27 +22,16 @@ namespace osu.Game.Screens.Play.HUD
private const float transition_duration = 200;
private readonly SongProgressBar bar;
private readonly SongProgressGraph graph;
private readonly DefaultSongProgressBar bar;
private readonly DefaultSongProgressGraph graph;
private readonly SongProgressInfo info;
/// <summary>
/// Whether seeking is allowed and the progress bar should be shown.
/// </summary>
public readonly Bindable<bool> AllowSeeking = new Bindable<bool>();
[SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")]
public Bindable<bool> ShowGraph { get; } = new BindableBool(true);
public override bool HandleNonPositionalInput => AllowSeeking.Value;
public override bool HandlePositionalInput => AllowSeeking.Value;
[Resolved]
private Player? player { get; set; }
[Resolved]
private DrawableRuleset? drawableRuleset { get; set; }
public DefaultSongProgress()
{
RelativeSizeAxes = Axes.X;
@ -58,7 +46,7 @@ namespace osu.Game.Screens.Play.HUD
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
},
graph = new SongProgressGraph
graph = new DefaultSongProgressGraph
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft,
@ -66,7 +54,7 @@ namespace osu.Game.Screens.Play.HUD
Height = graph_height,
Margin = new MarginPadding { Bottom = bottom_bar_height },
},
bar = new SongProgressBar(bottom_bar_height, graph_height, handle_size)
bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
@ -75,34 +63,18 @@ namespace osu.Game.Screens.Play.HUD
};
}
[BackgroundDependencyLoader(true)]
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
base.LoadComplete();
if (drawableRuleset != null)
{
if (player?.Configuration.AllowUserInteraction == true)
((IBindable<bool>)AllowSeeking).BindTo(drawableRuleset.HasReplayLoaded);
}
graph.FillColour = bar.FillColour = colours.BlueLighter;
}
protected override void LoadComplete()
{
AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true);
Interactive.BindValueChanged(_ => updateBarVisibility(), true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
}
protected override void PopIn()
{
this.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(100);
base.LoadComplete();
}
protected override void UpdateObjects(IEnumerable<HitObject> objects)
@ -133,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateBarVisibility()
{
bar.ShowHandle = AllowSeeking.Value;
bar.Interactive = Interactive.Value;
updateInfoMargin();
}
@ -150,7 +122,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateInfoMargin()
{
float finalMargin = bottom_bar_height + (AllowSeeking.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0);
float finalMargin = bottom_bar_height + (Interactive.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0);
info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In);
}
}

View File

@ -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 System;
using osuTK;
using osuTK.Graphics;
@ -15,17 +13,17 @@ using osu.Framework.Threading;
namespace osu.Game.Screens.Play.HUD
{
public partial class SongProgressBar : SliderBar<double>
public partial class DefaultSongProgressBar : SliderBar<double>
{
public Action<double> OnSeek;
/// <summary>
/// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation.
/// </summary>
public Action<double>? OnSeek { get; set; }
private readonly Box fill;
private readonly Container handleBase;
private readonly Container handleContainer;
private bool showHandle;
public bool ShowHandle
/// <summary>
/// Whether the progress bar should allow interaction, ie. to perform seek operations.
/// </summary>
public bool Interactive
{
get => showHandle;
set
@ -59,7 +57,13 @@ namespace osu.Game.Screens.Play.HUD
set => CurrentNumber.Value = value;
}
public SongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
private readonly Box fill;
private readonly Container handleBase;
private readonly Container handleContainer;
private bool showHandle;
public DefaultSongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
{
CurrentNumber.MinValue = 0;
CurrentNumber.MaxValue = 1;
@ -142,7 +146,7 @@ namespace osu.Game.Screens.Play.HUD
handleBase.X = newX;
}
private ScheduledDelegate scheduledSeek;
private ScheduledDelegate? scheduledSeek;
protected override void OnUserChange(double value)
{

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Play.HUD
{
public partial class SongProgressGraph : SquareGraph
public partial class DefaultSongProgressGraph : SquareGraph
{
private IEnumerable<HitObject> objects;

View File

@ -68,10 +68,12 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter
Mode.BindValueChanged(_ => updateMode(), true);
ShowMaxJudgement.BindValueChanged(value =>
ShowMaxJudgement.BindValueChanged(showMax =>
{
var firstChild = CounterFlow.Children.FirstOrDefault();
firstChild.FadeTo(value.NewValue ? 1 : 0, TRANSFORM_DURATION, Easing.OutQuint);
if (firstChild != null)
firstChild.State.Value = showMax.NewValue ? Visibility.Visible : Visibility.Hidden;
}, true);
}

View File

@ -4,6 +4,8 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Rulesets.Objects;
@ -16,10 +18,19 @@ namespace osu.Game.Screens.Play.HUD
{
// Some implementations of this element allow seeking during gameplay playback.
// Set a sane default of never handling input to override the behaviour provided by OverlayContainer.
public override bool HandleNonPositionalInput => false;
public override bool HandlePositionalInput => false;
public override bool HandleNonPositionalInput => Interactive.Value;
public override bool HandlePositionalInput => Interactive.Value;
protected override bool BlockScrollInput => false;
/// <summary>
/// Whether interaction should be allowed (ie. seeking). If <c>false</c>, interaction controls will not be displayed.
/// </summary>
/// <remarks>
/// By default, this will be automatically decided based on the gameplay state.
/// </remarks>
public readonly Bindable<bool> Interactive = new Bindable<bool>();
public bool UsesFixedAnchor { get; set; }
[Resolved]
@ -63,14 +74,21 @@ namespace osu.Game.Screens.Play.HUD
protected virtual void UpdateObjects(IEnumerable<HitObject> objects) { }
[BackgroundDependencyLoader]
private void load(DrawableRuleset? drawableRuleset)
private void load(DrawableRuleset? drawableRuleset, Player? player)
{
if (drawableRuleset != null)
{
if (player?.Configuration.AllowUserInteraction == true)
((IBindable<bool>)Interactive).BindTo(drawableRuleset.HasReplayLoaded);
Objects = drawableRuleset.Objects;
}
}
protected override void PopIn() => this.FadeIn(500, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(100);
protected override void Update()
{
base.Update();

View File

@ -10,6 +10,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using System;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Screens.Play.HUD
{
@ -27,13 +28,33 @@ namespace osu.Game.Screens.Play.HUD
private double songLength => endTime - startTime;
private const int margin = 10;
public FontUsage Font
{
set
{
timeCurrent.Font = value;
timeLeft.Font = value;
progress.Font = value;
}
}
public Colour4 TextColour
{
set
{
timeCurrent.Colour = value;
timeLeft.Colour = value;
progress.Colour = value;
}
}
public double StartTime
{
set => startTime = value;
}
public bool ShowProgress { get; init; } = true;
public double EndTime
{
set => endTime = value;
@ -76,6 +97,7 @@ namespace osu.Game.Screens.Play.HUD
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Alpha = ShowProgress ? 1 : 0,
Child = new UprightAspectMaintainingContainer
{
Origin = Anchor.Centre,
@ -99,15 +121,15 @@ namespace osu.Game.Screens.Play.HUD
AutoSizeAxes = Axes.Both,
Child = new UprightAspectMaintainingContainer
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Scaling = ScaleMode.Vertical,
ScalingFactor = 0.5f,
Child = timeLeft = new SizePreservingSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
Colour = colours.BlueLighter,
Font = OsuFont.Numeric,
}
@ -128,7 +150,7 @@ namespace osu.Game.Screens.Play.HUD
if (currentPercent != previousPercent)
{
progress.Text = currentPercent.ToString() + @"%";
progress.Text = currentPercent + @"%";
previousPercent = currentPercent;
}

View File

@ -61,50 +61,75 @@ namespace osu.Game.Screens.Select.Carousel
if (!(other is CarouselBeatmapSet otherSet))
return base.CompareTo(criteria, other);
int comparison = 0;
switch (criteria.Sort)
{
default:
case SortMode.Artist:
return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase);
comparison = string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase);
break;
case SortMode.Title:
return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase);
comparison = string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase);
break;
case SortMode.Author:
return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase);
comparison = string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase);
break;
case SortMode.Source:
return string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase);
comparison = string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase);
break;
case SortMode.DateAdded:
return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
break;
case SortMode.DateRanked:
// Beatmaps which have no ranked date should already be filtered away in this mode.
if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null)
return 0;
break;
return otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value);
comparison = otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value);
break;
case SortMode.LastPlayed:
return -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
comparison = -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
break;
case SortMode.BPM:
return compareUsingAggregateMax(otherSet, b => b.BPM);
comparison = compareUsingAggregateMax(otherSet, b => b.BPM);
break;
case SortMode.Length:
return compareUsingAggregateMax(otherSet, b => b.Length);
comparison = compareUsingAggregateMax(otherSet, b => b.Length);
break;
case SortMode.Difficulty:
return compareUsingAggregateMax(otherSet, b => b.StarRating);
comparison = compareUsingAggregateMax(otherSet, b => b.StarRating);
break;
case SortMode.DateSubmitted:
// Beatmaps which have no submitted date should already be filtered away in this mode.
if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null)
return 0;
break;
return otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
comparison = otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
break;
}
if (comparison != 0) return comparison;
// If the initial sort could not differentiate, attempt to use DateAdded to order sets in a stable fashion.
// The directionality of this matches the current SortMode.DateAdded, but we may want to reconsider if that becomes a user decision (ie. asc / desc).
comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
if (comparison != 0) return comparison;
// If DateAdded fails to break the tie, fallback to our internal GUID for stability.
// This basically means it's a stable random sort.
return otherSet.BeatmapSet.ID.CompareTo(BeatmapSet.ID);
}
/// <summary>

View File

@ -108,6 +108,7 @@ namespace osu.Game.Skinning
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
var songProgress = container.OfType<ArgonSongProgress>().FirstOrDefault();
if (score != null)
{
@ -158,6 +159,12 @@ namespace osu.Game.Skinning
// origin flipped to match scale above.
hitError2.Origin = Anchor.CentreLeft;
}
if (songProgress != null)
{
songProgress.Position = new Vector2(0, -10);
songProgress.Scale = new Vector2(0.9f, 1);
}
}
})
{
@ -167,7 +174,7 @@ namespace osu.Game.Skinning
new DefaultScoreCounter(),
new DefaultAccuracyCounter(),
new DefaultHealthDisplay(),
new DefaultSongProgress(),
new ArgonSongProgress(),
new BarHitErrorMeter(),
new BarHitErrorMeter(),
new PerformancePointsCounter()

View File

@ -65,11 +65,14 @@ namespace osu.Game.Skinning
default:
this.ScaleTo(0.6f).Then()
.ScaleTo(1.1f, fade_in_length * 0.8f).Then()
// this is actually correct to match stable; there were overlapping transforms.
.ScaleTo(0.9f).Delay(fade_in_length * 0.2f)
.ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then()
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f);
.ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8
.Delay(fade_in_length * 0.2f) // t = 1.0
.ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2
// stable dictates scale of 0.9->1 over time 1.0 to 1.4, but we are already at 1.2.
// so we need to force the current value to be correct at 1.2 (0.95) then complete the
// second half of the transform.
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4
break;
}
}

View File

@ -15,6 +15,10 @@ namespace osu.Game.Skinning
{
private CircularProgress circularProgress = null!;
// Legacy song progress doesn't support interaction for now.
public override bool HandleNonPositionalInput => false;
public override bool HandlePositionalInput => false;
[BackgroundDependencyLoader]
private void load()
{
@ -56,16 +60,6 @@ namespace osu.Game.Skinning
};
}
protected override void PopIn()
{
this.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(100);
}
protected override void UpdateProgress(double progress, bool isIntro)
{
if (isIntro)

View File

@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual
{
RelativeSizeAxes = Axes.Both,
BorderColour = Color4.White,
BorderThickness = 5,
BorderThickness = 3,
Masking = true,
Children = new Drawable[]
@ -142,8 +142,15 @@ namespace osu.Game.Tests.Visual
c.AutoSizeAxes = Axes.None;
c.Size = Vector2.Zero;
c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None;
c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None;
if (autoSize)
c.AutoSizeAxes = Axes.Both;
else
{
c.RelativeSizeAxes = Axes.Both;
c.Anchor = Anchor.Centre;
c.Origin = Anchor.Centre;
c.Size = new Vector2(0.97f);
}
}
outlineBox.Alpha = autoSize ? 1 : 0;