1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 18:03:11 +08:00

Merge branch 'master' into level-badge-colour

This commit is contained in:
Dean Herbert 2023-01-23 15:46:15 +09:00 committed by GitHub
commit 432e0a6a37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1616 additions and 261 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1226.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.120.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

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. // 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.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using osu.Framework.Input; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
@ -28,6 +27,7 @@ namespace osu.Game.Rulesets.Osu
/// </remarks> /// </remarks>
public bool AllowGameplayInputs public bool AllowGameplayInputs
{ {
get => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs;
set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value; 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) protected override bool Handle(UIEvent e)
{ {
if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false; if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
@ -52,19 +58,6 @@ namespace osu.Game.Rulesets.Osu
return base.Handle(e); 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 partial class OsuKeyBindingContainer : RulesetKeyBindingContainer
{ {
private bool allowGameplayInputs = true; private bool allowGameplayInputs = true;

View File

@ -35,14 +35,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public void SetRotation(float currentRotation) public void SetRotation(float currentRotation)
{ {
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
if (Precision.AlmostEquals(0, Time.Elapsed))
return;
// If we've gone back in time, it's fine to work with a fresh set of records for now // If we've gone back in time, it's fine to work with a fresh set of records for now
if (records.Count > 0 && Time.Current < records.Last().Time) if (records.Count > 0 && Time.Current < records.Last().Time)
records.Clear(); records.Clear();
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, records.Last().Time))
return;
if (records.Count > 0) if (records.Count > 0)
{ {
var record = records.Peek(); var record = records.Peek();

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", "Archives/modified-default-20220818.osk",
// Covers longest combo counter // Covers longest combo counter
"Archives/modified-default-20221012.osk", "Archives/modified-default-20221012.osk",
// Covers Argon variant of song progress bar
"Archives/modified-argon-20221024.osk",
// Covers TextElement and BeatmapInfoDrawable // Covers TextElement and BeatmapInfoDrawable
"Archives/modified-default-20221102.osk", "Archives/modified-default-20221102.osk",
// Covers BPM counter. // Covers BPM counter.

View File

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

View File

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

View File

@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestInputDoesntWorkWhenHUDHidden() public void TestInputDoesntWorkWhenHUDHidden()
{ {
SongProgressBar? getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().SingleOrDefault(); ArgonSongProgress? getSongProgress() => hudOverlay.ChildrenOfType<ArgonSongProgress>().SingleOrDefault();
bool seeked = false; bool seeked = false;
@ -204,8 +204,8 @@ namespace osu.Game.Tests.Visual.Gameplay
Debug.Assert(progress != null); Debug.Assert(progress != null);
progress.ShowHandle = true; progress.Interactive.Value = true;
progress.OnSeek += _ => seeked = true; progress.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += _ => seeked = true;
}); });
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); 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 JudgementTally judgementTally = null!;
private TestJudgementCounterDisplay counterDisplay = null!; private TestJudgementCounterDisplay counterDisplay = null!;
private DependencyProvidingContainer content = null!;
protected override Container<Drawable> Content => content;
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>(); private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
private int iteration; private int iteration;
[SetUpSteps] [SetUpSteps]
public void SetupSteps() => AddStep("Create components", () => public void SetUpSteps() => AddStep("Create components", () =>
{ {
var ruleset = CreateRuleset(); var ruleset = CreateRuleset();
Debug.Assert(ruleset != null); Debug.Assert(ruleset != null);
scoreProcessor = new ScoreProcessor(ruleset); scoreProcessor = new ScoreProcessor(ruleset);
Child = new DependencyProvidingContainer base.Content.Child = new DependencyProvidingContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) }, CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) },
Children = new Drawable[] Children = new Drawable[]
{ {
judgementTally = new JudgementTally(), judgementTally = new JudgementTally(),
new DependencyProvidingContainer content = new DependencyProvidingContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) }, 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] [Test]
public void TestAddJudgementsToCounters() public void TestAddJudgementsToCounters()
{ {
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Great), 2); AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Great), 2);
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Miss), 2); AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Miss), 2);
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Meh), 2); AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Meh), 2);
@ -86,6 +86,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestAddWhilstHidden() public void TestAddWhilstHidden()
{ {
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2); AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2);
AddAssert("Check value added whilst hidden", () => hiddenCount() == 2); AddAssert("Check value added whilst hidden", () => hiddenCount() == 2);
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All); AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
@ -94,6 +96,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestChangeFlowDirection() public void TestChangeFlowDirection()
{ {
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical); AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical);
AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal); AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal);
} }
@ -101,6 +105,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestToggleJudgementNames() public void TestToggleJudgementNames()
{ {
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = false); AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = false);
AddWaitStep("wait some", 2); AddWaitStep("wait some", 2);
AddAssert("Assert hidden", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 0); AddAssert("Assert hidden", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 0);
@ -112,15 +118,40 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestHideMaxValue() public void TestHideMaxValue()
{ {
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false); AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false);
AddWaitStep("wait some", 2); AddWaitStep("wait some", 2);
AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0); AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
AddStep("Show max judgement", () => counterDisplay.ShowMaxJudgement.Value = true); 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 TestMaxValueHiddenOnModeChange()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Set max judgement to hide itself", () => counterDisplay.ShowMaxJudgement.Value = false);
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
AddWaitStep("wait some", 2);
AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
}
[Test] [Test]
public void TestCycleDisplayModes() public void TestCycleDisplayModes()
{ {
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple); AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple);
AddWaitStep("wait some", 2); AddWaitStep("wait some", 2);
AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 0); AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 0);
@ -139,6 +170,13 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestJudgementCounterDisplay : JudgementCounterDisplay private partial class TestJudgementCounterDisplay : JudgementCounterDisplay
{ {
public new FillFlowContainer<JudgementCounter> CounterFlow => base.CounterFlow; 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. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -28,50 +28,62 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); 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<IGameplayClock>(gameplayClockContainer);
Dependencies.CacheAs<IFrameStableClock>(frameStabilityContainer);
} }
[SetUpSteps] [SetUpSteps]
public void SetupSteps() public void SetupSteps()
{ {
AddStep("reset clock", () => gameplayClockContainer.Reset()); 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] [Test]
public void TestDisplay() public void TestBasic()
{ {
AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time)); AddToggleStep("toggle seeking", b =>
AddStep("start", gameplayClockContainer.Start); {
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); AddStep("stop", gameplayClockContainer.Stop);
} }
[Test] private void applyToArgonProgress(Action<ArgonSongProgress> action) =>
public void TestToggleSeeking() this.ChildrenOfType<ArgonSongProgress>().ForEach(action);
{
void applyToDefaultProgress(Action<DefaultSongProgress> action) => private void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
this.ChildrenOfType<DefaultSongProgress>().ForEach(action); this.ChildrenOfType<DefaultSongProgress>().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);
}
protected override Drawable CreateDefaultImplementation() => new DefaultSongProgress(); protected override Drawable CreateDefaultImplementation() => new DefaultSongProgress();
protected override Drawable CreateArgonImplementation() => new ArgonSongProgress();
protected override Drawable CreateLegacyImplementation() => new LegacySongProgress(); 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 => AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() && !p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
!p.ChildrenOfType<HoldForMenuButton>().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)); AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
} }

View File

@ -11,6 +11,8 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -21,6 +23,7 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Comments; using osu.Game.Overlays.Comments;
using osu.Game.Overlays.Comments.Buttons;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
@ -259,7 +262,7 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any()); AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any());
AddStep("Set report data", () => AddStep("Set report data", () =>
{ {
var field = this.ChildrenOfType<OsuTextBox>().Single(); var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
field.Current.Value = report_text; field.Current.Value = report_text;
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single(); var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
reason.Current.Value = CommentReportReason.Other; reason.Current.Value = CommentReportReason.Other;
@ -278,6 +281,93 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Request is correct", () => request != null && request.CommentID == 2 && request.Comment == report_text && request.Reason == CommentReportReason.Other); AddAssert("Request is correct", () => request != null && request.CommentID == 2 && request.Comment == report_text && request.Reason == CommentReportReason.Other);
} }
[Test]
public void TestReply()
{
addTestComments();
DrawableComment? targetComment = null;
AddUntilStep("Comment exists", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
targetComment = comments.SingleOrDefault(x => x.Comment.Id == 2);
return targetComment != null;
});
AddStep("Setup request handling", () =>
{
requestLock.Reset();
dummyAPI.HandleRequest = r =>
{
if (!(r is CommentPostRequest req))
return false;
if (req.ParentCommentId != 2)
throw new ArgumentException("Wrong parent ID in request!");
if (req.CommentableId != 123 || req.Commentable != CommentableType.Beatmapset)
throw new ArgumentException("Wrong commentable data in request!");
Task.Run(() =>
{
requestLock.Wait(10000);
req.TriggerSuccess(new CommentBundle
{
Comments = new List<Comment>
{
new Comment
{
Id = 98,
Message = req.Message,
LegacyName = "FirstUser",
CreatedAt = DateTimeOffset.Now,
VotesCount = 98,
ParentId = req.ParentCommentId,
}
}
});
});
return true;
};
});
AddStep("Click reply button", () =>
{
var btn = targetComment.ChildrenOfType<LinkFlowContainer>().Skip(1).First();
var texts = btn.ChildrenOfType<SpriteText>();
InputManager.MoveMouseTo(texts.Skip(1).First());
InputManager.Click(MouseButton.Left);
});
AddAssert("There is 0 replies", () =>
{
var replLabel = targetComment.ChildrenOfType<ShowRepliesButton>().First().ChildrenOfType<SpriteText>().First();
return replLabel.Text.ToString().Contains('0') && targetComment!.Comment.RepliesCount == 0;
});
AddStep("Focus field", () =>
{
InputManager.MoveMouseTo(targetComment.ChildrenOfType<TextBox>().First());
InputManager.Click(MouseButton.Left);
});
AddStep("Enter text", () =>
{
targetComment.ChildrenOfType<TextBox>().First().Current.Value = "random reply";
});
AddStep("Submit", () =>
{
InputManager.Key(Key.Enter);
});
AddStep("Complete request", () => requestLock.Set());
AddUntilStep("There is 1 reply", () =>
{
var replLabel = targetComment.ChildrenOfType<ShowRepliesButton>().First().ChildrenOfType<SpriteText>().First();
return replLabel.Text.ToString().Contains('1') && targetComment!.Comment.RepliesCount == 1;
});
AddUntilStep("Submitted comment shown", () =>
{
var r = targetComment.ChildrenOfType<DrawableComment>().Skip(1).FirstOrDefault();
return r != null && r.Comment.Message == "random reply";
});
}
private void addTestComments() private void addTestComments()
{ {
AddStep("set up response", () => AddStep("set up response", () =>

View File

@ -89,6 +89,7 @@ namespace osu.Game.Tests.Visual.Online
Groups = new[] Groups = new[]
{ {
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" }, new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "mania" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } } new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
}, },
ProfileOrder = new[] ProfileOrder = new[]

View File

@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("Leaderboard shows two aggregate scores", () => match.ChildrenOfType<MatchLeaderboardScore>().Count(s => s.ScoreText.Text != "0") == 2); AddUntilStep("Leaderboard shows two aggregate scores", () => match.ChildrenOfType<MatchLeaderboardScore>().Count(s => s.ScoreText.Text != "0") == 2);
AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().TriggerClick()); ClickButtonWhenEnabled<PlaylistsReadyButton>();
AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader); AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader);
} }

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

View File

@ -1064,7 +1064,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("options enabled", () => songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value); AddAssert("options enabled", () => songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
AddStep("delete all beatmaps", () => manager.Delete()); AddStep("delete all beatmaps", () => manager.Delete());
AddWaitStep("wait for debounce", 1); AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault);
AddAssert("options disabled", () => !songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value); AddAssert("options disabled", () => !songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; 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 200", () => beatmapDensity());
AddStep("beatmap density with granularity of 300", () => beatmapDensity(300)); AddStep("beatmap density with granularity of 300", () => beatmapDensity(300));
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().ToArray()); AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().ToArray());
AddStep("change colour", () => AddStep("change tier colours", () =>
{ {
graph.TierColours = new[] graph.TierColours = new[]
{ {
@ -62,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Colour4.Blue Colour4.Blue
}; };
}); });
AddStep("reset colour", () => AddStep("reset tier colours", () =>
{ {
graph.TierColours = new[] graph.TierColours = new[]
{ {
@ -74,6 +75,12 @@ namespace osu.Game.Tests.Visual.UserInterface
Colour4.Green 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) private void sinFunction(int size = 100)

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Tests
{ {
Colour = OsuColour.Gray(0.5f), Colour = OsuColour.Gray(0.5f),
Depth = 10 Depth = 10
}, AddInternal); }, Add);
// Have to construct this here, rather than in the constructor, because // Have to construct this here, rather than in the constructor, because
// we depend on some dependencies to be loaded within OsuGameBase.load(). // we depend on some dependencies to be loaded within OsuGameBase.load().

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tournament
private async Task checkForChanges() private async Task checkForChanges()
{ {
string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder()).ConfigureAwait(false); string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder()).ConfigureAwait(true);
// If a save hasn't been triggered by the user yet, populate the initial value // If a save hasn't been triggered by the user yet, populate the initial value
lastSerialisedLadder ??= serialisedLadder; lastSerialisedLadder ??= serialisedLadder;

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tournament
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(Storage baseStorage) private void load(Storage baseStorage)
{ {
AddInternal(initialisationText = new TournamentSpriteText Add(initialisationText = new TournamentSpriteText
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -250,12 +250,15 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
if (Masking)
BorderThickness = 3; BorderThickness = 3;
base.OnFocus(e); base.OnFocus(e);
} }
protected override void OnFocusLost(FocusLostEvent e) protected override void OnFocusLost(FocusLostEvent e)
{ {
if (Masking)
BorderThickness = 0; BorderThickness = 0;
base.OnFocusLost(e); base.OnFocusLost(e);

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders; 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; get => tierColours;
set set
{ {
if (value.Length == 0 || value == tierColours) tierCount = value.Count;
return;
tierCount = value.Length;
tierColours = value; tierColours = value;
graphNeedsUpdate = true; graphNeedsUpdate = true;
@ -154,8 +152,6 @@ namespace osu.Game.Graphics.UserInterface
segments.Sort(); segments.Sort();
} }
private Colour4 getTierColour(int tier) => tier >= 0 ? tierColours[tier] : new Colour4(0, 0, 0, 0);
protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this); protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this);
protected struct SegmentInfo protected struct SegmentInfo
@ -203,6 +199,7 @@ namespace osu.Game.Graphics.UserInterface
private IShader shader = null!; private IShader shader = null!;
private readonly List<SegmentInfo> segments = new List<SegmentInfo>(); private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
private Vector2 drawSize; private Vector2 drawSize;
private readonly List<Colour4> tierColours = new List<Colour4>();
public SegmentedGraphDrawNode(SegmentedGraph<T> source) public SegmentedGraphDrawNode(SegmentedGraph<T> source)
: base(source) : base(source)
@ -216,8 +213,12 @@ namespace osu.Game.Graphics.UserInterface
texture = Source.texture; texture = Source.texture;
shader = Source.shader; shader = Source.shader;
drawSize = Source.DrawSize; drawSize = Source.DrawSize;
segments.Clear(); segments.Clear();
segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1)); segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1));
tierColours.Clear();
tierColours.AddRange(Source.tierColours);
} }
public override void Draw(IRenderer renderer) public override void Draw(IRenderer renderer)
@ -240,11 +241,27 @@ namespace osu.Game.Graphics.UserInterface
Vector2Extensions.Transform(topRight, DrawInfo.Matrix), Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix), Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)), Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)),
Source.getTierColour(segment.Tier)); getSegmentColour(segment));
} }
shader.Unbind(); shader.Unbind();
} }
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 class SegmentManager : IEnumerable<SegmentInfo> protected class SegmentManager : IEnumerable<SegmentInfo>

View File

@ -160,9 +160,12 @@ namespace osu.Game
protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method
/// <summary>
/// The current ruleset selection for the local user.
/// </summary>
[Cached] [Cached]
[Cached(typeof(IBindable<RulesetInfo>))] [Cached(typeof(IBindable<RulesetInfo>))]
protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>(); protected internal readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
/// <summary> /// <summary>
/// The current mod selection for the local user. /// The current mod selection for the local user.

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using Humanizer; using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -15,7 +13,12 @@ namespace osu.Game.Overlays.Comments.Buttons
public ShowRepliesButton(int count) public ShowRepliesButton(int count)
{ {
Text = "reply".ToQuantity(count); Count = count;
}
public int Count
{
set => Text = "reply".ToQuantity(value);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -35,6 +35,8 @@ namespace osu.Game.Overlays.Comments
private RoundedButton commitButton = null!; private RoundedButton commitButton = null!;
private LoadingSpinner loadingSpinner = null!; private LoadingSpinner loadingSpinner = null!;
protected TextBox TextBox { get; private set; } = null!;
protected bool ShowLoadingSpinner protected bool ShowLoadingSpinner
{ {
set set
@ -51,8 +53,6 @@ namespace osu.Game.Overlays.Comments
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
{ {
EditorTextBox textBox;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Masking = true; Masking = true;
@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Comments
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
textBox = new EditorTextBox TextBox = new EditorTextBox
{ {
Height = 40, Height = 40,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Comments
} }
}); });
textBox.OnCommit += (_, _) => commitButton.TriggerClick(); TextBox.OnCommit += (_, _) => commitButton.TriggerClick();
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Comments
private void updateCommitButtonState() => private void updateCommitButtonState() =>
commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value); commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value);
private partial class EditorTextBox : BasicTextBox private partial class EditorTextBox : OsuTextBox
{ {
protected override float LeftRightPadding => side_padding; protected override float LeftRightPadding => side_padding;
@ -173,12 +173,6 @@ namespace osu.Game.Overlays.Comments
{ {
Font = OsuFont.GetFont(weight: FontWeight.Regular), Font = OsuFont.GetFont(weight: FontWeight.Regular),
}; };
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
{
AutoSizeAxes = Axes.Both,
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }
};
} }
protected partial class EditorButton : RoundedButton protected partial class EditorButton : RoundedButton

View File

@ -301,7 +301,7 @@ namespace osu.Game.Overlays.Comments
void addNewComment(Comment comment) void addNewComment(Comment comment)
{ {
var drawableComment = getDrawableComment(comment); var drawableComment = GetDrawableComment(comment);
if (comment.ParentId == null) if (comment.ParentId == null)
{ {
@ -333,7 +333,7 @@ namespace osu.Game.Overlays.Comments
if (CommentDictionary.ContainsKey(comment.Id)) if (CommentDictionary.ContainsKey(comment.Id))
continue; continue;
topLevelComments.Add(getDrawableComment(comment)); topLevelComments.Add(GetDrawableComment(comment));
} }
if (topLevelComments.Any()) if (topLevelComments.Any())
@ -351,7 +351,7 @@ namespace osu.Game.Overlays.Comments
} }
} }
private DrawableComment getDrawableComment(Comment comment) public DrawableComment GetDrawableComment(Comment comment)
{ {
if (CommentDictionary.TryGetValue(comment.Id, out var existing)) if (CommentDictionary.TryGetValue(comment.Id, out var existing))
return existing; return existing;

View File

@ -22,6 +22,7 @@ using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -74,6 +75,7 @@ namespace osu.Game.Overlays.Comments
private OsuSpriteText deletedLabel = null!; private OsuSpriteText deletedLabel = null!;
private GridContainer content = null!; private GridContainer content = null!;
private VotePill votePill = null!; private VotePill votePill = null!;
private Container<CommentEditor> replyEditorContainer = null!;
[Resolved] [Resolved]
private IDialogOverlay? dialogOverlay { get; set; } private IDialogOverlay? dialogOverlay { get; set; }
@ -232,6 +234,12 @@ namespace osu.Game.Overlays.Comments
} }
} }
}, },
replyEditorContainer = new Container<CommentEditor>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Top = 10 },
},
new Container new Container
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -254,6 +262,7 @@ namespace osu.Game.Overlays.Comments
}, },
childCommentsVisibilityContainer = new FillFlowContainer childCommentsVisibilityContainer = new FillFlowContainer
{ {
Name = @"Children comments",
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
@ -344,6 +353,8 @@ namespace osu.Game.Overlays.Comments
actionsContainer.AddLink(CommonStrings.ButtonsPermalink, copyUrl); actionsContainer.AddLink(CommonStrings.ButtonsPermalink, copyUrl);
actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10)); actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
actionsContainer.AddLink(CommonStrings.ButtonsReply.ToLower(), toggleReply);
actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id) if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id)
actionsContainer.AddLink(CommonStrings.ButtonsDelete.ToLower(), deleteComment); actionsContainer.AddLink(CommonStrings.ButtonsDelete.ToLower(), deleteComment);
@ -419,8 +430,9 @@ namespace osu.Game.Overlays.Comments
if (!ShowDeleted.Value) if (!ShowDeleted.Value)
Hide(); Hide();
}); });
request.Failure += _ => Schedule(() => request.Failure += e => Schedule(() =>
{ {
Logger.Error(e, "Failed to delete comment");
actionsLoading.Hide(); actionsLoading.Hide();
actionsContainer.Show(); actionsContainer.Show();
}); });
@ -433,6 +445,26 @@ namespace osu.Game.Overlays.Comments
onScreenDisplay?.Display(new CopyUrlToast()); onScreenDisplay?.Display(new CopyUrlToast());
} }
private void toggleReply()
{
if (replyEditorContainer.Count == 0)
{
replyEditorContainer.Add(new ReplyCommentEditor(Comment)
{
OnPost = comments =>
{
Comment.RepliesCount += comments.Length;
showRepliesButton.Count = Comment.RepliesCount;
Replies.AddRange(comments);
}
});
}
else
{
replyEditorContainer.Clear(true);
}
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
ShowDeleted.BindValueChanged(show => ShowDeleted.BindValueChanged(show =>
@ -445,8 +477,6 @@ namespace osu.Game.Overlays.Comments
base.LoadComplete(); base.LoadComplete();
} }
public bool ContainsReply(long replyId) => loadedReplies.ContainsKey(replyId);
private void onRepliesAdded(IEnumerable<DrawableComment> replies) private void onRepliesAdded(IEnumerable<DrawableComment> replies)
{ {
var page = createRepliesPage(replies); var page = createRepliesPage(replies);

View File

@ -0,0 +1,70 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments
{
public partial class ReplyCommentEditor : CancellableCommentEditor
{
[Resolved]
private CommentsContainer commentsContainer { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Comment parentComment;
public Action<DrawableComment[]>? OnPost;
protected override LocalisableString FooterText => default;
protected override LocalisableString CommitButtonText => CommonStrings.ButtonsReply;
protected override LocalisableString TextBoxPlaceholder => CommentsStrings.PlaceholderReply;
public ReplyCommentEditor(Comment parent)
{
parentComment = parent;
OnCancel = () => this.FadeOut(200).Expire();
}
protected override void LoadComplete()
{
base.LoadComplete();
GetContainingInputManager().ChangeFocus(TextBox);
}
protected override void OnCommit(string text)
{
ShowLoadingSpinner = true;
CommentPostRequest req = new CommentPostRequest(commentsContainer.Type.Value, commentsContainer.Id.Value, text, parentComment.Id);
req.Failure += e => Schedule(() =>
{
ShowLoadingSpinner = false;
Logger.Error(e, "Posting reply comment failed.");
});
req.Success += cb => Schedule(processPostedComments, cb);
api.Queue(req);
}
private void processPostedComments(CommentBundle cb)
{
foreach (var comment in cb.Comments)
comment.ParentComment = parentComment;
var drawables = cb.Comments.Select(commentsContainer.GetDrawableComment).ToArray();
OnPost?.Invoke(drawables);
OnCancel!.Invoke();
}
}
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
public partial class GroupBadge : Container, IHasTooltip public partial class GroupBadge : Container, IHasTooltip
{ {
public LocalisableString TooltipText { get; } public LocalisableString TooltipText { get; private set; }
public int TextSize { get; set; } = 12; public int TextSize { get; set; } = 12;
@ -78,6 +78,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
icon.Size = new Vector2(TextSize - 1); icon.Size = new Vector2(TextSize - 1);
})).ToList() })).ToList()
); );
var badgeModesList = group.Playmodes.Select(p => rulesets.GetRuleset(p)?.Name).ToList();
string modesDisplay = string.Join(", ", badgeModesList);
TooltipText += $" ({modesDisplay})";
} }
} }
} }

View File

@ -133,6 +133,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsSlider<float> new SettingsSlider<float>
{ {
LabelText = GraphicsSettingsStrings.HorizontalPosition, LabelText = GraphicsSettingsStrings.HorizontalPosition,
Keywords = new[] { "screen", "scaling" },
Current = scalingPositionX, Current = scalingPositionX,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
@ -140,6 +141,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsSlider<float> new SettingsSlider<float>
{ {
LabelText = GraphicsSettingsStrings.VerticalPosition, LabelText = GraphicsSettingsStrings.VerticalPosition,
Keywords = new[] { "screen", "scaling" },
Current = scalingPositionY, Current = scalingPositionY,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
@ -147,6 +149,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsSlider<float> new SettingsSlider<float>
{ {
LabelText = GraphicsSettingsStrings.HorizontalScale, LabelText = GraphicsSettingsStrings.HorizontalScale,
Keywords = new[] { "screen", "scaling" },
Current = scalingSizeX, Current = scalingSizeX,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
@ -154,6 +157,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsSlider<float> new SettingsSlider<float>
{ {
LabelText = GraphicsSettingsStrings.VerticalScale, LabelText = GraphicsSettingsStrings.VerticalScale,
Keywords = new[] { "screen", "scaling" },
Current = scalingSizeY, Current = scalingSizeY,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true

View File

@ -51,6 +51,7 @@ namespace osu.Game.Screens.Play
private const float duration = 2500; private const float duration = 2500;
private ISample? failSample; private ISample? failSample;
private SampleChannel? failSampleChannel;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -119,13 +120,13 @@ namespace osu.Game.Screens.Play
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ => this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
{ {
// Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep. // Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep.
RemoveFilters(false); removeFilters(false);
OnComplete?.Invoke(); OnComplete?.Invoke();
}); });
failHighPassFilter.CutoffTo(300); failHighPassFilter.CutoffTo(300);
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
failSample?.Play(); failSampleChannel = failSample?.Play();
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
@ -153,7 +154,16 @@ namespace osu.Game.Screens.Play
Background?.FadeColour(OsuColour.Gray(0.3f), 60); Background?.FadeColour(OsuColour.Gray(0.3f), 60);
} }
public void RemoveFilters(bool resetTrackFrequency = true) /// <summary>
/// Stops any and all persistent effects added by the ongoing fail animation.
/// </summary>
public void Stop()
{
failSampleChannel?.Stop();
removeFilters();
}
private void removeFilters(bool resetTrackFrequency = true)
{ {
filtersRemoved = true; filtersRemoved = true;

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

@ -15,14 +15,12 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
[Resolved] [Resolved]
private IGameplayClock gameplayClock { get; set; } = null!; private IGameplayClock gameplayClock { get; set; } = null!;
[Resolved(canBeNull: true)] [Resolved]
private DrawableRuleset? drawableRuleset { get; set; } private IFrameStableClock? frameStableClock { get; set; }
public int Value { get; private set; } public int Value { get; private set; }
// Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset` private IGameplayClock clock => frameStableClock ?? gameplayClock;
// as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency.
private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock;
public ClicksPerSecondCalculator() public ClicksPerSecondCalculator()
{ {

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osuTK; using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
@ -23,27 +22,16 @@ namespace osu.Game.Screens.Play.HUD
private const float transition_duration = 200; private const float transition_duration = 200;
private readonly SongProgressBar bar; private readonly DefaultSongProgressBar bar;
private readonly SongProgressGraph graph; private readonly DefaultSongProgressGraph graph;
private readonly SongProgressInfo info; 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")] [SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")]
public Bindable<bool> ShowGraph { get; } = new BindableBool(true); public Bindable<bool> ShowGraph { get; } = new BindableBool(true);
public override bool HandleNonPositionalInput => AllowSeeking.Value;
public override bool HandlePositionalInput => AllowSeeking.Value;
[Resolved] [Resolved]
private Player? player { get; set; } private Player? player { get; set; }
[Resolved]
private DrawableRuleset? drawableRuleset { get; set; }
public DefaultSongProgress() public DefaultSongProgress()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -58,7 +46,7 @@ namespace osu.Game.Screens.Play.HUD
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
}, },
graph = new SongProgressGraph graph = new DefaultSongProgressGraph
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
@ -66,7 +54,7 @@ namespace osu.Game.Screens.Play.HUD
Height = graph_height, Height = graph_height,
Margin = new MarginPadding { Bottom = bottom_bar_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, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
@ -75,34 +63,18 @@ namespace osu.Game.Screens.Play.HUD
}; };
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader]
private void load(OsuColour colours) 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; graph.FillColour = bar.FillColour = colours.BlueLighter;
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true); Interactive.BindValueChanged(_ => updateBarVisibility(), true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
}
protected override void PopIn() base.LoadComplete();
{
this.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(100);
} }
protected override void UpdateObjects(IEnumerable<HitObject> objects) protected override void UpdateObjects(IEnumerable<HitObject> objects)
@ -133,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateBarVisibility() private void updateBarVisibility()
{ {
bar.ShowHandle = AllowSeeking.Value; bar.Interactive = Interactive.Value;
updateInfoMargin(); updateInfoMargin();
} }
@ -150,7 +122,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateInfoMargin() 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); 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. // 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.
#nullable disable
using System; using System;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -15,17 +13,17 @@ using osu.Framework.Threading;
namespace osu.Game.Screens.Play.HUD 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; /// <summary>
private readonly Container handleBase; /// Whether the progress bar should allow interaction, ie. to perform seek operations.
private readonly Container handleContainer; /// </summary>
public bool Interactive
private bool showHandle;
public bool ShowHandle
{ {
get => showHandle; get => showHandle;
set set
@ -59,7 +57,13 @@ namespace osu.Game.Screens.Play.HUD
set => CurrentNumber.Value = value; 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.MinValue = 0;
CurrentNumber.MaxValue = 1; CurrentNumber.MaxValue = 1;
@ -142,7 +146,7 @@ namespace osu.Game.Screens.Play.HUD
handleBase.X = newX; handleBase.X = newX;
} }
private ScheduledDelegate scheduledSeek; private ScheduledDelegate? scheduledSeek;
protected override void OnUserChange(double value) protected override void OnUserChange(double value)
{ {

View File

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

View File

@ -2,7 +2,6 @@
// 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 System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -66,27 +65,27 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter
counter.Direction.Value = convertedDirection; counter.Direction.Value = convertedDirection;
}, true); }, true);
Mode.BindValueChanged(_ => updateMode(), true); Mode.BindValueChanged(_ => updateDisplay());
ShowMaxJudgement.BindValueChanged(_ => updateDisplay(), true);
ShowMaxJudgement.BindValueChanged(value =>
{
var firstChild = CounterFlow.Children.FirstOrDefault();
firstChild.FadeTo(value.NewValue ? 1 : 0, TRANSFORM_DURATION, Easing.OutQuint);
}, true);
} }
private void updateMode() private void updateDisplay()
{ {
foreach (var counter in CounterFlow.Children) for (int i = 0; i < CounterFlow.Children.Count; i++)
{ {
if (shouldShow(counter)) JudgementCounter counter = CounterFlow.Children[i];
if (shouldShow(i, counter))
counter.Show(); counter.Show();
else else
counter.Hide(); counter.Hide();
} }
bool shouldShow(JudgementCounter counter) bool shouldShow(int index, JudgementCounter counter)
{ {
if (index == 0 && !ShowMaxJudgement.Value)
return false;
if (counter.Result.Type.IsBasic()) if (counter.Result.Type.IsBasic())
return true; return true;

View File

@ -4,6 +4,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -16,19 +18,33 @@ namespace osu.Game.Screens.Play.HUD
{ {
// Some implementations of this element allow seeking during gameplay playback. // 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. // Set a sane default of never handling input to override the behaviour provided by OverlayContainer.
public override bool HandleNonPositionalInput => false; public override bool HandleNonPositionalInput => Interactive.Value;
public override bool HandlePositionalInput => false; public override bool HandlePositionalInput => Interactive.Value;
protected override bool BlockScrollInput => false; 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; } public bool UsesFixedAnchor { get; set; }
[Resolved] [Resolved]
protected IGameplayClock GameplayClock { get; private set; } = null!; protected IGameplayClock GameplayClock { get; private set; } = null!;
[Resolved(canBeNull: true)] [Resolved]
private DrawableRuleset? drawableRuleset { get; set; } private IFrameStableClock? frameStableClock { get; set; }
/// <summary>
/// The reference clock is used to accurately tell the current playfield's time (including catch-up lag).
/// However, if none is available (i.e. used in tests), we fall back to the gameplay clock.
/// </summary>
protected IClock FrameStableClock => frameStableClock ?? GameplayClock;
private IClock? referenceClock;
private IEnumerable<HitObject>? objects; private IEnumerable<HitObject>? objects;
public IEnumerable<HitObject> Objects public IEnumerable<HitObject> Objects
@ -58,15 +74,21 @@ namespace osu.Game.Screens.Play.HUD
protected virtual void UpdateObjects(IEnumerable<HitObject> objects) { } protected virtual void UpdateObjects(IEnumerable<HitObject> objects) { }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(DrawableRuleset? drawableRuleset, Player? player)
{ {
if (drawableRuleset != null) if (drawableRuleset != null)
{ {
if (player?.Configuration.AllowUserInteraction == true)
((IBindable<bool>)Interactive).BindTo(drawableRuleset.HasReplayLoaded);
Objects = drawableRuleset.Objects; Objects = drawableRuleset.Objects;
referenceClock = drawableRuleset.FrameStableClock;
} }
} }
protected override void PopIn() => this.FadeIn(500, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(100);
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -74,9 +96,7 @@ namespace osu.Game.Screens.Play.HUD
if (objects == null) if (objects == null)
return; return;
// The reference clock is used to accurately tell the playfield's time. This is obtained from the drawable ruleset. double currentTime = FrameStableClock.CurrentTime;
// However, if no drawable ruleset is available (i.e. used in tests), we fall back to the gameplay clock.
double currentTime = referenceClock?.CurrentTime ?? GameplayClock.CurrentTime;
bool isInIntro = currentTime < FirstHitTime; bool isInIntro = currentTime < FirstHitTime;

View File

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

View File

@ -309,6 +309,8 @@ namespace osu.Game.Screens.Play
}); });
} }
dependencies.CacheAs(DrawableRuleset.FrameStableClock);
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
@ -1070,7 +1072,7 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(ScreenExitEvent e) public override bool OnExiting(ScreenExitEvent e)
{ {
screenSuspension?.RemoveAndDisposeImmediately(); screenSuspension?.RemoveAndDisposeImmediately();
failAnimationLayer?.RemoveFilters(); failAnimationLayer?.Stop();
if (LoadedBeatmapSuccessfully) if (LoadedBeatmapSuccessfully)
{ {

View File

@ -61,50 +61,75 @@ namespace osu.Game.Screens.Select.Carousel
if (!(other is CarouselBeatmapSet otherSet)) if (!(other is CarouselBeatmapSet otherSet))
return base.CompareTo(criteria, other); return base.CompareTo(criteria, other);
int comparison = 0;
switch (criteria.Sort) switch (criteria.Sort)
{ {
default: default:
case SortMode.Artist: 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: 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: 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: 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: case SortMode.DateAdded:
return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded); comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
break;
case SortMode.DateRanked: case SortMode.DateRanked:
// Beatmaps which have no ranked date should already be filtered away in this mode. // Beatmaps which have no ranked date should already be filtered away in this mode.
if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null) 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: 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: case SortMode.BPM:
return compareUsingAggregateMax(otherSet, b => b.BPM); comparison = compareUsingAggregateMax(otherSet, b => b.BPM);
break;
case SortMode.Length: case SortMode.Length:
return compareUsingAggregateMax(otherSet, b => b.Length); comparison = compareUsingAggregateMax(otherSet, b => b.Length);
break;
case SortMode.Difficulty: case SortMode.Difficulty:
return compareUsingAggregateMax(otherSet, b => b.StarRating); comparison = compareUsingAggregateMax(otherSet, b => b.StarRating);
break;
case SortMode.DateSubmitted: case SortMode.DateSubmitted:
// Beatmaps which have no submitted date should already be filtered away in this mode. // Beatmaps which have no submitted date should already be filtered away in this mode.
if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null) 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> /// <summary>

View File

@ -30,14 +30,16 @@ namespace osu.Game.Screens.Select.Details
{ {
public partial class AdvancedStats : Container public partial class AdvancedStats : Container
{ {
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
[Resolved] [Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } private IBindable<IReadOnlyList<Mod>> mods { get; set; }
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } private OsuGameBase game { get; set; }
[Resolved] private IBindable<RulesetInfo> gameRuleset;
private BeatmapDifficultyCache difficultyCache { get; set; }
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
private readonly StatisticRow starDifficulty; private readonly StatisticRow starDifficulty;
@ -84,7 +86,13 @@ namespace osu.Game.Screens.Select.Details
{ {
base.LoadComplete(); base.LoadComplete();
ruleset.BindValueChanged(_ => updateStatistics()); // the cached ruleset bindable might be a decoupled bindable provided by SongSelect,
// which we can't rely on in combination with the game-wide selected mods list,
// since mods could be updated to the new ruleset instances while the decoupled bindable is held behind,
// therefore resulting in performing difficulty calculation with invalid states.
gameRuleset = game.Ruleset.GetBoundCopy();
gameRuleset.BindValueChanged(_ => updateStatistics());
mods.BindValueChanged(modsChanged, true); mods.BindValueChanged(modsChanged, true);
} }
@ -142,7 +150,14 @@ namespace osu.Game.Screens.Select.Details
private CancellationTokenSource starDifficultyCancellationSource; private CancellationTokenSource starDifficultyCancellationSource;
private void updateStarDifficulty() /// <summary>
/// Updates the displayed star difficulty statistics with the values provided by the currently-selected beatmap, ruleset, and selected mods.
/// </summary>
/// <remarks>
/// This is scheduled to avoid scenarios wherein a ruleset changes first before selected mods do,
/// potentially resulting in failure during difficulty calculation due to incomplete bindable state updates.
/// </remarks>
private void updateStarDifficulty() => Scheduler.AddOnce(() =>
{ {
starDifficultyCancellationSource?.Cancel(); starDifficultyCancellationSource?.Cancel();
@ -151,8 +166,8 @@ namespace osu.Game.Screens.Select.Details
starDifficultyCancellationSource = new CancellationTokenSource(); starDifficultyCancellationSource = new CancellationTokenSource();
var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, null, starDifficultyCancellationSource.Token); var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, gameRuleset.Value, null, starDifficultyCancellationSource.Token);
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, gameRuleset.Value, mods.Value, starDifficultyCancellationSource.Token);
Task.WhenAll(normalStarDifficultyTask, moddedStarDifficultyTask).ContinueWith(_ => Schedule(() => Task.WhenAll(normalStarDifficultyTask, moddedStarDifficultyTask).ContinueWith(_ => Schedule(() =>
{ {
@ -164,7 +179,7 @@ namespace osu.Game.Screens.Select.Details
starDifficulty.Value = ((float)normalDifficulty.Value.Stars, (float)moddedDifficulty.Value.Stars); starDifficulty.Value = ((float)normalDifficulty.Value.Stars, (float)moddedDifficulty.Value.Stars);
}), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current); }), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
} });
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {

View File

@ -108,6 +108,7 @@ namespace osu.Game.Skinning
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault(); var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault(); var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault(); var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
var songProgress = container.OfType<ArgonSongProgress>().FirstOrDefault();
if (score != null) if (score != null)
{ {
@ -158,6 +159,12 @@ namespace osu.Game.Skinning
// origin flipped to match scale above. // origin flipped to match scale above.
hitError2.Origin = Anchor.CentreLeft; 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 DefaultScoreCounter(),
new DefaultAccuracyCounter(), new DefaultAccuracyCounter(),
new DefaultHealthDisplay(), new DefaultHealthDisplay(),
new DefaultSongProgress(), new ArgonSongProgress(),
new BarHitErrorMeter(), new BarHitErrorMeter(),
new BarHitErrorMeter(), new BarHitErrorMeter(),
new PerformancePointsCounter() new PerformancePointsCounter()

View File

@ -65,11 +65,14 @@ namespace osu.Game.Skinning
default: default:
this.ScaleTo(0.6f).Then() this.ScaleTo(0.6f).Then()
.ScaleTo(1.1f, fade_in_length * 0.8f).Then() .ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8
// this is actually correct to match stable; there were overlapping transforms. .Delay(fade_in_length * 0.2f) // t = 1.0
.ScaleTo(0.9f).Delay(fade_in_length * 0.2f) .ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2
.ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then()
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // 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; break;
} }
} }

View File

@ -15,6 +15,10 @@ namespace osu.Game.Skinning
{ {
private CircularProgress circularProgress = null!; private CircularProgress circularProgress = null!;
// Legacy song progress doesn't support interaction for now.
public override bool HandleNonPositionalInput => false;
public override bool HandlePositionalInput => false;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() 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) protected override void UpdateProgress(double progress, bool isIntro)
{ {
if (isIntro) if (isIntro)

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tests
{ {
Depth = 10, Depth = 10,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, AddInternal); }, Add);
// Have to construct this here, rather than in the constructor, because // Have to construct this here, rather than in the constructor, because
// we depend on some dependencies to be loaded within OsuGameBase.load(). // we depend on some dependencies to be loaded within OsuGameBase.load().

View File

@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
BorderColour = Color4.White, BorderColour = Color4.White,
BorderThickness = 5, BorderThickness = 3,
Masking = true, Masking = true,
Children = new Drawable[] Children = new Drawable[]
@ -142,8 +142,15 @@ namespace osu.Game.Tests.Visual
c.AutoSizeAxes = Axes.None; c.AutoSizeAxes = Axes.None;
c.Size = Vector2.Zero; c.Size = Vector2.Zero;
c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None; if (autoSize)
c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None; 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; outlineBox.Alpha = autoSize ? 1 : 0;

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.18.0" /> <PackageReference Include="Realm" Version="10.18.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.1226.0" /> <PackageReference Include="ppy.osu.Framework" Version="2023.120.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1221.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1221.0" />
<PackageReference Include="Sentry" Version="3.23.1" /> <PackageReference Include="Sentry" Version="3.23.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -16,6 +16,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1226.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2023.120.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>