mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 11:42:54 +08:00
Merge branch 'master' into level-badge-colour
This commit is contained in:
commit
432e0a6a37
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1226.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.120.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
405
osu.Game.Rulesets.Osu.Tests/TestSceneTouchInput.cs
Normal file
405
osu.Game.Rulesets.Osu.Tests/TestSceneTouchInput.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges.Events;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu
|
||||
@ -28,6 +27,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
/// </remarks>
|
||||
public bool AllowGameplayInputs
|
||||
{
|
||||
get => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs;
|
||||
set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value;
|
||||
}
|
||||
|
||||
@ -45,6 +45,12 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Add(new OsuTouchInputMapper(this) { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
protected override bool Handle(UIEvent e)
|
||||
{
|
||||
if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
|
||||
@ -52,19 +58,6 @@ namespace osu.Game.Rulesets.Osu
|
||||
return base.Handle(e);
|
||||
}
|
||||
|
||||
protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
|
||||
{
|
||||
if (!AllowUserCursorMovement)
|
||||
{
|
||||
// Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse.
|
||||
// Primarily relied upon by the "autopilot" osu! mod.
|
||||
var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position);
|
||||
e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null);
|
||||
}
|
||||
|
||||
return base.HandleMouseTouchStateChange(e);
|
||||
}
|
||||
|
||||
private partial class OsuKeyBindingContainer : RulesetKeyBindingContainer
|
||||
{
|
||||
private bool allowGameplayInputs = true;
|
||||
|
@ -35,14 +35,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
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 (records.Count > 0 && Time.Current < records.Last().Time)
|
||||
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)
|
||||
{
|
||||
var record = records.Peek();
|
||||
|
104
osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs
Normal file
104
osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20221024.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20221024.osk
Normal file
Binary file not shown.
@ -41,6 +41,8 @@ namespace osu.Game.Tests.Skins
|
||||
"Archives/modified-default-20220818.osk",
|
||||
// Covers longest combo counter
|
||||
"Archives/modified-default-20221012.osk",
|
||||
// Covers Argon variant of song progress bar
|
||||
"Archives/modified-argon-20221024.osk",
|
||||
// Covers TextElement and BeatmapInfoDrawable
|
||||
"Archives/modified-default-20221102.osk",
|
||||
// Covers BPM counter.
|
||||
|
@ -20,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
var implementation = skin is LegacySkin
|
||||
? CreateLegacyImplementation()
|
||||
: CreateDefaultImplementation();
|
||||
: skin is ArgonSkin
|
||||
? CreateArgonImplementation()
|
||||
: CreateDefaultImplementation();
|
||||
|
||||
implementation.Anchor = Anchor.Centre;
|
||||
implementation.Origin = Anchor.Centre;
|
||||
@ -29,6 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
|
||||
protected abstract Drawable CreateDefaultImplementation();
|
||||
protected virtual Drawable CreateArgonImplementation() => CreateDefaultImplementation();
|
||||
protected abstract Drawable CreateLegacyImplementation();
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ using osu.Game.Screens.Play.HUD;
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneSongProgressGraph : OsuTestScene
|
||||
public partial class TestSceneDefaultSongProgressGraph : OsuTestScene
|
||||
{
|
||||
private TestSongProgressGraph graph;
|
||||
|
||||
@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
graph.Objects = objects;
|
||||
}
|
||||
|
||||
private partial class TestSongProgressGraph : SongProgressGraph
|
||||
private partial class TestSongProgressGraph : DefaultSongProgressGraph
|
||||
{
|
||||
public int CreationCount { get; private set; }
|
||||
|
@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestInputDoesntWorkWhenHUDHidden()
|
||||
{
|
||||
SongProgressBar? getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().SingleOrDefault();
|
||||
ArgonSongProgress? getSongProgress() => hudOverlay.ChildrenOfType<ArgonSongProgress>().SingleOrDefault();
|
||||
|
||||
bool seeked = false;
|
||||
|
||||
@ -204,8 +204,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
Debug.Assert(progress != null);
|
||||
|
||||
progress.ShowHandle = true;
|
||||
progress.OnSeek += _ => seeked = true;
|
||||
progress.Interactive.Value = true;
|
||||
progress.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += _ => seeked = true;
|
||||
});
|
||||
|
||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
||||
|
@ -25,35 +25,33 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private JudgementTally judgementTally = null!;
|
||||
private TestJudgementCounterDisplay counterDisplay = null!;
|
||||
|
||||
private DependencyProvidingContainer content = null!;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
|
||||
|
||||
private int iteration;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetupSteps() => AddStep("Create components", () =>
|
||||
public void SetUpSteps() => AddStep("Create components", () =>
|
||||
{
|
||||
var ruleset = CreateRuleset();
|
||||
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
scoreProcessor = new ScoreProcessor(ruleset);
|
||||
Child = new DependencyProvidingContainer
|
||||
base.Content.Child = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
judgementTally = new JudgementTally(),
|
||||
new DependencyProvidingContainer
|
||||
content = new DependencyProvidingContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) },
|
||||
Child = counterDisplay = new TestJudgementCounterDisplay
|
||||
{
|
||||
Margin = new MarginPadding { Top = 100 },
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -78,6 +76,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestAddJudgementsToCounters()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
|
||||
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Great), 2);
|
||||
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Miss), 2);
|
||||
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.Meh), 2);
|
||||
@ -86,6 +86,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestAddWhilstHidden()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
|
||||
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2);
|
||||
AddAssert("Check value added whilst hidden", () => hiddenCount() == 2);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
@ -94,6 +96,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestChangeFlowDirection()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
|
||||
AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical);
|
||||
AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal);
|
||||
}
|
||||
@ -101,6 +105,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestToggleJudgementNames()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
|
||||
AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = false);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Assert hidden", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 0);
|
||||
@ -112,15 +118,40 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestHideMaxValue()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
|
||||
AddStep("Hide max judgement", () => counterDisplay.ShowMaxJudgement.Value = false);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
|
||||
AddStep("Show max judgement", () => counterDisplay.ShowMaxJudgement.Value = true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMaxValueStartsHidden()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay
|
||||
{
|
||||
ShowMaxJudgement = { Value = false }
|
||||
});
|
||||
AddAssert("Check max hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void 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]
|
||||
public void TestCycleDisplayModes()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
|
||||
AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 0);
|
||||
@ -139,6 +170,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private partial class TestJudgementCounterDisplay : JudgementCounterDisplay
|
||||
{
|
||||
public new FillFlowContainer<JudgementCounter> CounterFlow => base.CounterFlow;
|
||||
|
||||
public TestJudgementCounterDisplay()
|
||||
{
|
||||
Margin = new MarginPadding { Top = 100 };
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,14 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
@ -28,50 +28,62 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
|
||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time));
|
||||
FrameStabilityContainer frameStabilityContainer;
|
||||
|
||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
|
||||
{
|
||||
Child = frameStabilityContainer = new FrameStabilityContainer
|
||||
{
|
||||
MaxCatchUpFrames = 1
|
||||
}
|
||||
});
|
||||
|
||||
Dependencies.CacheAs<IGameplayClock>(gameplayClockContainer);
|
||||
Dependencies.CacheAs<IFrameStableClock>(frameStabilityContainer);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetupSteps()
|
||||
{
|
||||
AddStep("reset clock", () => gameplayClockContainer.Reset());
|
||||
AddStep("set hit objects", setHitObjects);
|
||||
AddStep("set hit objects", () => this.ChildrenOfType<SongProgress>().ForEach(progress => progress.Objects = Beatmap.Value.Beatmap.HitObjects));
|
||||
AddStep("hook seeking", () =>
|
||||
{
|
||||
applyToDefaultProgress(d => d.ChildrenOfType<DefaultSongProgressBar>().Single().OnSeek += t => gameplayClockContainer.Seek(t));
|
||||
applyToArgonProgress(d => d.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += t => gameplayClockContainer.Seek(t));
|
||||
});
|
||||
AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time));
|
||||
AddStep("start", () => gameplayClockContainer.Start());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("seek to intro", () => gameplayClockContainer.Seek(skip_target_time));
|
||||
AddStep("start", gameplayClockContainer.Start);
|
||||
AddToggleStep("toggle seeking", b =>
|
||||
{
|
||||
applyToDefaultProgress(s => s.Interactive.Value = b);
|
||||
applyToArgonProgress(s => s.Interactive.Value = b);
|
||||
});
|
||||
|
||||
AddToggleStep("toggle graph", b =>
|
||||
{
|
||||
applyToDefaultProgress(s => s.ShowGraph.Value = b);
|
||||
applyToArgonProgress(s => s.ShowGraph.Value = b);
|
||||
});
|
||||
|
||||
AddStep("stop", gameplayClockContainer.Stop);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestToggleSeeking()
|
||||
{
|
||||
void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
|
||||
this.ChildrenOfType<DefaultSongProgress>().ForEach(action);
|
||||
private void applyToArgonProgress(Action<ArgonSongProgress> action) =>
|
||||
this.ChildrenOfType<ArgonSongProgress>().ForEach(action);
|
||||
|
||||
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
|
||||
AddStep("hide graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = false));
|
||||
AddStep("disallow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = false));
|
||||
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
|
||||
AddStep("show graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = true));
|
||||
}
|
||||
|
||||
private void setHitObjects()
|
||||
{
|
||||
var objects = new List<HitObject>();
|
||||
for (double i = 0; i < 5000; i++)
|
||||
objects.Add(new HitObject { StartTime = i });
|
||||
|
||||
this.ChildrenOfType<SongProgress>().ForEach(progress => progress.Objects = objects);
|
||||
}
|
||||
private void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
|
||||
this.ChildrenOfType<DefaultSongProgress>().ForEach(action);
|
||||
|
||||
protected override Drawable CreateDefaultImplementation() => new DefaultSongProgress();
|
||||
|
||||
protected override Drawable CreateArgonImplementation() => new ArgonSongProgress();
|
||||
|
||||
protected override Drawable CreateLegacyImplementation() => new LegacySongProgress();
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
|
||||
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
|
||||
!p.ChildrenOfType<HoldForMenuButton>().Any() &&
|
||||
p.ChildrenOfType<SongProgressBar>().SingleOrDefault()?.ShowHandle == false));
|
||||
p.ChildrenOfType<ArgonSongProgressBar>().SingleOrDefault()?.Interactive == false));
|
||||
|
||||
AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.Containers;
|
||||
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.Overlays;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osu.Game.Overlays.Comments.Buttons;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
@ -259,7 +262,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any());
|
||||
AddStep("Set report data", () =>
|
||||
{
|
||||
var field = this.ChildrenOfType<OsuTextBox>().Single();
|
||||
var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
|
||||
field.Current.Value = report_text;
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
|
||||
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);
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
AddStep("set up response", () =>
|
||||
|
@ -89,6 +89,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Groups = new[]
|
||||
{
|
||||
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" } }
|
||||
},
|
||||
ProfileOrder = new[]
|
||||
|
@ -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);
|
||||
|
||||
AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().TriggerClick());
|
||||
ClickButtonWhenEnabled<PlaylistsReadyButton>();
|
||||
AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader);
|
||||
}
|
||||
|
||||
|
@ -580,10 +580,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
/// Ensures stability is maintained on different sort modes for items with equal properties.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestSortingStability()
|
||||
public void TestSortingStabilityDateAdded()
|
||||
{
|
||||
var sets = new List<BeatmapSetInfo>();
|
||||
int idOffset = 0;
|
||||
|
||||
AddStep("Populuate beatmap sets", () =>
|
||||
{
|
||||
@ -593,38 +592,34 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo();
|
||||
|
||||
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(i);
|
||||
|
||||
// only need to set the first as they are a shared reference.
|
||||
var beatmap = set.Beatmaps.First();
|
||||
|
||||
beatmap.Metadata.Artist = $"artist {i / 2}";
|
||||
beatmap.Metadata.Title = $"title {9 - i}";
|
||||
beatmap.Metadata.Artist = "a";
|
||||
beatmap.Metadata.Title = "b";
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
idOffset = sets.First().OnlineID;
|
||||
});
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
|
||||
|
||||
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
||||
AddAssert("Items are in reverse order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + sets.Count - index - 1).All(b => b));
|
||||
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
|
||||
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddAssert("Items reset to original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
|
||||
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestSortingStabilityWithNewItems()
|
||||
public void TestSortingStabilityWithRemovedAndReaddedItem()
|
||||
{
|
||||
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||
int idOffset = 0;
|
||||
|
||||
AddStep("Populuate beatmap sets", () =>
|
||||
{
|
||||
@ -640,16 +635,68 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
beatmap.Metadata.Artist = "same artist";
|
||||
beatmap.Metadata.Title = "same title";
|
||||
|
||||
// testing the case where DateAdded happens to equal (quite rare).
|
||||
set.DateAdded = DateTimeOffset.UnixEpoch;
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
idOffset = sets.First().OnlineID;
|
||||
});
|
||||
|
||||
Guid[] originalOrder = null!;
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
assertOriginalOrderMaintained();
|
||||
|
||||
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
|
||||
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
|
||||
|
||||
AddStep("Remove item", () => carousel.RemoveBeatmapSet(sets[1]));
|
||||
AddStep("Re-add item", () => carousel.UpdateBeatmapSet(sets[1]));
|
||||
|
||||
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
|
||||
|
||||
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
||||
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures stability is maintained on different sort modes while a new item is added to the carousel.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestSortingStabilityWithNewItems()
|
||||
{
|
||||
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||
|
||||
AddStep("Populuate beatmap sets", () =>
|
||||
{
|
||||
sets.Clear();
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(3);
|
||||
|
||||
// only need to set the first as they are a shared reference.
|
||||
var beatmap = set.Beatmaps.First();
|
||||
|
||||
beatmap.Metadata.Artist = "same artist";
|
||||
beatmap.Metadata.Title = "same title";
|
||||
|
||||
// testing the case where DateAdded happens to equal (quite rare).
|
||||
set.DateAdded = DateTimeOffset.UnixEpoch;
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
});
|
||||
|
||||
Guid[] originalOrder = null!;
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
|
||||
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
|
||||
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
|
||||
|
||||
AddStep("Add new item", () =>
|
||||
{
|
||||
@ -661,19 +708,18 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
beatmap.Metadata.Artist = "same artist";
|
||||
beatmap.Metadata.Title = "same title";
|
||||
|
||||
set.DateAdded = DateTimeOffset.FromUnixTimeSeconds(1);
|
||||
|
||||
carousel.UpdateBeatmapSet(set);
|
||||
|
||||
// add set to expected ordering
|
||||
originalOrder = originalOrder.Prepend(set.ID).ToArray();
|
||||
});
|
||||
|
||||
assertOriginalOrderMaintained();
|
||||
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
|
||||
|
||||
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
||||
assertOriginalOrderMaintained();
|
||||
|
||||
void assertOriginalOrderMaintained()
|
||||
{
|
||||
AddAssert("Items remain in original order",
|
||||
() => carousel.BeatmapSets.Select(s => s.OnlineID), () => Is.EqualTo(carousel.BeatmapSets.Select((set, index) => idOffset + index)));
|
||||
}
|
||||
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -1064,7 +1064,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddAssert("options enabled", () => songSelect.ChildrenOfType<FooterButtonOptions>().Single().Enabled.Value);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("beatmap density with granularity of 200", () => beatmapDensity());
|
||||
AddStep("beatmap density with granularity of 300", () => beatmapDensity(300));
|
||||
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().ToArray());
|
||||
AddStep("change colour", () =>
|
||||
AddStep("change tier colours", () =>
|
||||
{
|
||||
graph.TierColours = new[]
|
||||
{
|
||||
@ -62,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Colour4.Blue
|
||||
};
|
||||
});
|
||||
AddStep("reset colour", () =>
|
||||
AddStep("reset tier colours", () =>
|
||||
{
|
||||
graph.TierColours = new[]
|
||||
{
|
||||
@ -74,6 +75,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Colour4.Green
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("set graph colour to blue", () => graph.Colour = Colour4.Blue);
|
||||
AddStep("set graph colour to transparent", () => graph.Colour = Colour4.Transparent);
|
||||
AddStep("set graph colour to vertical gradient", () => graph.Colour = ColourInfo.GradientVertical(Colour4.White, Colour4.Black));
|
||||
AddStep("set graph colour to horizontal gradient", () => graph.Colour = ColourInfo.GradientHorizontal(Colour4.White, Colour4.Black));
|
||||
AddStep("reset graph colour", () => graph.Colour = Colour4.White);
|
||||
}
|
||||
|
||||
private void sinFunction(int size = 100)
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Tests
|
||||
{
|
||||
Colour = OsuColour.Gray(0.5f),
|
||||
Depth = 10
|
||||
}, AddInternal);
|
||||
}, Add);
|
||||
|
||||
// Have to construct this here, rather than in the constructor, because
|
||||
// we depend on some dependencies to be loaded within OsuGameBase.load().
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Tournament
|
||||
|
||||
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
|
||||
lastSerialisedLadder ??= serialisedLadder;
|
||||
|
@ -58,7 +58,7 @@ namespace osu.Game.Tournament
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(Storage baseStorage)
|
||||
{
|
||||
AddInternal(initialisationText = new TournamentSpriteText
|
||||
Add(initialisationText = new TournamentSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -250,13 +250,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
BorderThickness = 3;
|
||||
if (Masking)
|
||||
BorderThickness = 3;
|
||||
|
||||
base.OnFocus(e);
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
{
|
||||
BorderThickness = 0;
|
||||
if (Masking)
|
||||
BorderThickness = 0;
|
||||
|
||||
base.OnFocusLost(e);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
@ -48,17 +49,14 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private Colour4[] tierColours;
|
||||
private IReadOnlyList<Colour4> tierColours;
|
||||
|
||||
public Colour4[] TierColours
|
||||
public IReadOnlyList<Colour4> TierColours
|
||||
{
|
||||
get => tierColours;
|
||||
set
|
||||
{
|
||||
if (value.Length == 0 || value == tierColours)
|
||||
return;
|
||||
|
||||
tierCount = value.Length;
|
||||
tierCount = value.Count;
|
||||
tierColours = value;
|
||||
|
||||
graphNeedsUpdate = true;
|
||||
@ -154,8 +152,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
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 struct SegmentInfo
|
||||
@ -203,6 +199,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private IShader shader = null!;
|
||||
private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
|
||||
private Vector2 drawSize;
|
||||
private readonly List<Colour4> tierColours = new List<Colour4>();
|
||||
|
||||
public SegmentedGraphDrawNode(SegmentedGraph<T> source)
|
||||
: base(source)
|
||||
@ -216,8 +213,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
texture = Source.texture;
|
||||
shader = Source.shader;
|
||||
drawSize = Source.DrawSize;
|
||||
|
||||
segments.Clear();
|
||||
segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1));
|
||||
|
||||
tierColours.Clear();
|
||||
tierColours.AddRange(Source.tierColours);
|
||||
}
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
@ -240,11 +241,27 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)),
|
||||
Source.getTierColour(segment.Tier));
|
||||
getSegmentColour(segment));
|
||||
}
|
||||
|
||||
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>
|
||||
|
@ -160,9 +160,12 @@ namespace osu.Game
|
||||
|
||||
protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method
|
||||
|
||||
/// <summary>
|
||||
/// The current ruleset selection for the local user.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
[Cached(typeof(IBindable<RulesetInfo>))]
|
||||
protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
|
||||
protected internal readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// The current mod selection for the local user.
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using Humanizer;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Events;
|
||||
@ -15,7 +13,12 @@ namespace osu.Game.Overlays.Comments.Buttons
|
||||
|
||||
public ShowRepliesButton(int count)
|
||||
{
|
||||
Text = "reply".ToQuantity(count);
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
set => Text = "reply".ToQuantity(value);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -35,6 +35,8 @@ namespace osu.Game.Overlays.Comments
|
||||
private RoundedButton commitButton = null!;
|
||||
private LoadingSpinner loadingSpinner = null!;
|
||||
|
||||
protected TextBox TextBox { get; private set; } = null!;
|
||||
|
||||
protected bool ShowLoadingSpinner
|
||||
{
|
||||
set
|
||||
@ -51,8 +53,6 @@ namespace osu.Game.Overlays.Comments
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
EditorTextBox textBox;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Masking = true;
|
||||
@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Comments
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
textBox = new EditorTextBox
|
||||
TextBox = new EditorTextBox
|
||||
{
|
||||
Height = 40,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
@ -133,7 +133,7 @@ namespace osu.Game.Overlays.Comments
|
||||
}
|
||||
});
|
||||
|
||||
textBox.OnCommit += (_, _) => commitButton.TriggerClick();
|
||||
TextBox.OnCommit += (_, _) => commitButton.TriggerClick();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Comments
|
||||
private void updateCommitButtonState() =>
|
||||
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;
|
||||
|
||||
@ -173,12 +173,6 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
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
|
||||
|
@ -301,7 +301,7 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
void addNewComment(Comment comment)
|
||||
{
|
||||
var drawableComment = getDrawableComment(comment);
|
||||
var drawableComment = GetDrawableComment(comment);
|
||||
|
||||
if (comment.ParentId == null)
|
||||
{
|
||||
@ -333,7 +333,7 @@ namespace osu.Game.Overlays.Comments
|
||||
if (CommentDictionary.ContainsKey(comment.Id))
|
||||
continue;
|
||||
|
||||
topLevelComments.Add(getDrawableComment(comment));
|
||||
topLevelComments.Add(GetDrawableComment(comment));
|
||||
}
|
||||
|
||||
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))
|
||||
return existing;
|
||||
|
@ -22,6 +22,7 @@ using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
@ -74,6 +75,7 @@ namespace osu.Game.Overlays.Comments
|
||||
private OsuSpriteText deletedLabel = null!;
|
||||
private GridContainer content = null!;
|
||||
private VotePill votePill = null!;
|
||||
private Container<CommentEditor> replyEditorContainer = null!;
|
||||
|
||||
[Resolved]
|
||||
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
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@ -254,6 +262,7 @@ namespace osu.Game.Overlays.Comments
|
||||
},
|
||||
childCommentsVisibilityContainer = new FillFlowContainer
|
||||
{
|
||||
Name = @"Children comments",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
@ -344,6 +353,8 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
actionsContainer.AddLink(CommonStrings.ButtonsPermalink, copyUrl);
|
||||
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)
|
||||
actionsContainer.AddLink(CommonStrings.ButtonsDelete.ToLower(), deleteComment);
|
||||
@ -419,8 +430,9 @@ namespace osu.Game.Overlays.Comments
|
||||
if (!ShowDeleted.Value)
|
||||
Hide();
|
||||
});
|
||||
request.Failure += _ => Schedule(() =>
|
||||
request.Failure += e => Schedule(() =>
|
||||
{
|
||||
Logger.Error(e, "Failed to delete comment");
|
||||
actionsLoading.Hide();
|
||||
actionsContainer.Show();
|
||||
});
|
||||
@ -433,6 +445,26 @@ namespace osu.Game.Overlays.Comments
|
||||
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()
|
||||
{
|
||||
ShowDeleted.BindValueChanged(show =>
|
||||
@ -445,8 +477,6 @@ namespace osu.Game.Overlays.Comments
|
||||
base.LoadComplete();
|
||||
}
|
||||
|
||||
public bool ContainsReply(long replyId) => loadedReplies.ContainsKey(replyId);
|
||||
|
||||
private void onRepliesAdded(IEnumerable<DrawableComment> replies)
|
||||
{
|
||||
var page = createRepliesPage(replies);
|
||||
|
70
osu.Game/Overlays/Comments/ReplyCommentEditor.cs
Normal file
70
osu.Game/Overlays/Comments/ReplyCommentEditor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public partial class GroupBadge : Container, IHasTooltip
|
||||
{
|
||||
public LocalisableString TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; private set; }
|
||||
|
||||
public int TextSize { get; set; } = 12;
|
||||
|
||||
@ -78,6 +78,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
icon.Size = new Vector2(TextSize - 1);
|
||||
})).ToList()
|
||||
);
|
||||
|
||||
var badgeModesList = group.Playmodes.Select(p => rulesets.GetRuleset(p)?.Name).ToList();
|
||||
|
||||
string modesDisplay = string.Join(", ", badgeModesList);
|
||||
TooltipText += $" ({modesDisplay})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +133,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.HorizontalPosition,
|
||||
Keywords = new[] { "screen", "scaling" },
|
||||
Current = scalingPositionX,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
@ -140,6 +141,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.VerticalPosition,
|
||||
Keywords = new[] { "screen", "scaling" },
|
||||
Current = scalingPositionY,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
@ -147,6 +149,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.HorizontalScale,
|
||||
Keywords = new[] { "screen", "scaling" },
|
||||
Current = scalingSizeX,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
@ -154,6 +157,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.VerticalScale,
|
||||
Keywords = new[] { "screen", "scaling" },
|
||||
Current = scalingSizeY,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
|
@ -51,6 +51,7 @@ namespace osu.Game.Screens.Play
|
||||
private const float duration = 2500;
|
||||
|
||||
private ISample? failSample;
|
||||
private SampleChannel? failSampleChannel;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@ -119,13 +120,13 @@ namespace osu.Game.Screens.Play
|
||||
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
|
||||
{
|
||||
// Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep.
|
||||
RemoveFilters(false);
|
||||
removeFilters(false);
|
||||
OnComplete?.Invoke();
|
||||
});
|
||||
|
||||
failHighPassFilter.CutoffTo(300);
|
||||
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
|
||||
failSample?.Play();
|
||||
failSampleChannel = failSample?.Play();
|
||||
|
||||
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
|
||||
track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||
@ -153,7 +154,16 @@ namespace osu.Game.Screens.Play
|
||||
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;
|
||||
|
||||
|
117
osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
Normal file
117
osu.Game/Screens/Play/HUD/ArgonSongProgress.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
266
osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
Normal file
266
osu.Game/Screens/Play/HUD/ArgonSongProgressBar.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
64
osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs
Normal file
64
osu.Game/Screens/Play/HUD/ArgonSongProgressGraph.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,14 +15,12 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
|
||||
[Resolved]
|
||||
private IGameplayClock gameplayClock { get; set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DrawableRuleset? drawableRuleset { get; set; }
|
||||
[Resolved]
|
||||
private IFrameStableClock? frameStableClock { get; set; }
|
||||
|
||||
public int Value { get; private set; }
|
||||
|
||||
// Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset`
|
||||
// as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency.
|
||||
private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock;
|
||||
private IGameplayClock clock => frameStableClock ?? gameplayClock;
|
||||
|
||||
public ClicksPerSecondCalculator()
|
||||
{
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
@ -23,27 +22,16 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private const float transition_duration = 200;
|
||||
|
||||
private readonly SongProgressBar bar;
|
||||
private readonly SongProgressGraph graph;
|
||||
private readonly DefaultSongProgressBar bar;
|
||||
private readonly DefaultSongProgressGraph graph;
|
||||
private readonly SongProgressInfo info;
|
||||
|
||||
/// <summary>
|
||||
/// Whether seeking is allowed and the progress bar should be shown.
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> AllowSeeking = new Bindable<bool>();
|
||||
|
||||
[SettingSource("Show difficulty graph", "Whether a graph displaying difficulty throughout the beatmap should be shown")]
|
||||
public Bindable<bool> ShowGraph { get; } = new BindableBool(true);
|
||||
|
||||
public override bool HandleNonPositionalInput => AllowSeeking.Value;
|
||||
public override bool HandlePositionalInput => AllowSeeking.Value;
|
||||
|
||||
[Resolved]
|
||||
private Player? player { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private DrawableRuleset? drawableRuleset { get; set; }
|
||||
|
||||
public DefaultSongProgress()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@ -58,7 +46,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Anchor = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
graph = new SongProgressGraph
|
||||
graph = new DefaultSongProgressGraph
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Origin = Anchor.BottomLeft,
|
||||
@ -66,7 +54,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Height = graph_height,
|
||||
Margin = new MarginPadding { Bottom = bottom_bar_height },
|
||||
},
|
||||
bar = new SongProgressBar(bottom_bar_height, graph_height, handle_size)
|
||||
bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
@ -75,34 +63,18 @@ namespace osu.Game.Screens.Play.HUD
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (drawableRuleset != null)
|
||||
{
|
||||
if (player?.Configuration.AllowUserInteraction == true)
|
||||
((IBindable<bool>)AllowSeeking).BindTo(drawableRuleset.HasReplayLoaded);
|
||||
}
|
||||
|
||||
graph.FillColour = bar.FillColour = colours.BlueLighter;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true);
|
||||
Interactive.BindValueChanged(_ => updateBarVisibility(), true);
|
||||
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(100);
|
||||
base.LoadComplete();
|
||||
}
|
||||
|
||||
protected override void UpdateObjects(IEnumerable<HitObject> objects)
|
||||
@ -133,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void updateBarVisibility()
|
||||
{
|
||||
bar.ShowHandle = AllowSeeking.Value;
|
||||
bar.Interactive = Interactive.Value;
|
||||
|
||||
updateInfoMargin();
|
||||
}
|
||||
@ -150,7 +122,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void updateInfoMargin()
|
||||
{
|
||||
float finalMargin = bottom_bar_height + (AllowSeeking.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0);
|
||||
float finalMargin = bottom_bar_height + (Interactive.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0);
|
||||
info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -15,17 +13,17 @@ using osu.Framework.Threading;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class SongProgressBar : SliderBar<double>
|
||||
public partial class DefaultSongProgressBar : SliderBar<double>
|
||||
{
|
||||
public Action<double> OnSeek;
|
||||
/// <summary>
|
||||
/// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation.
|
||||
/// </summary>
|
||||
public Action<double>? OnSeek { get; set; }
|
||||
|
||||
private readonly Box fill;
|
||||
private readonly Container handleBase;
|
||||
private readonly Container handleContainer;
|
||||
|
||||
private bool showHandle;
|
||||
|
||||
public bool ShowHandle
|
||||
/// <summary>
|
||||
/// Whether the progress bar should allow interaction, ie. to perform seek operations.
|
||||
/// </summary>
|
||||
public bool Interactive
|
||||
{
|
||||
get => showHandle;
|
||||
set
|
||||
@ -59,7 +57,13 @@ namespace osu.Game.Screens.Play.HUD
|
||||
set => CurrentNumber.Value = value;
|
||||
}
|
||||
|
||||
public SongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
|
||||
private readonly Box fill;
|
||||
private readonly Container handleBase;
|
||||
private readonly Container handleContainer;
|
||||
|
||||
private bool showHandle;
|
||||
|
||||
public DefaultSongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
|
||||
{
|
||||
CurrentNumber.MinValue = 0;
|
||||
CurrentNumber.MaxValue = 1;
|
||||
@ -142,7 +146,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
handleBase.X = newX;
|
||||
}
|
||||
|
||||
private ScheduledDelegate scheduledSeek;
|
||||
private ScheduledDelegate? scheduledSeek;
|
||||
|
||||
protected override void OnUserChange(double value)
|
||||
{
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class SongProgressGraph : SquareGraph
|
||||
public partial class DefaultSongProgressGraph : SquareGraph
|
||||
{
|
||||
private IEnumerable<HitObject> objects;
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -66,27 +65,27 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
||||
counter.Direction.Value = convertedDirection;
|
||||
}, true);
|
||||
|
||||
Mode.BindValueChanged(_ => updateMode(), true);
|
||||
|
||||
ShowMaxJudgement.BindValueChanged(value =>
|
||||
{
|
||||
var firstChild = CounterFlow.Children.FirstOrDefault();
|
||||
firstChild.FadeTo(value.NewValue ? 1 : 0, TRANSFORM_DURATION, Easing.OutQuint);
|
||||
}, true);
|
||||
Mode.BindValueChanged(_ => updateDisplay());
|
||||
ShowMaxJudgement.BindValueChanged(_ => updateDisplay(), 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();
|
||||
else
|
||||
counter.Hide();
|
||||
}
|
||||
|
||||
bool shouldShow(JudgementCounter counter)
|
||||
bool shouldShow(int index, JudgementCounter counter)
|
||||
{
|
||||
if (index == 0 && !ShowMaxJudgement.Value)
|
||||
return false;
|
||||
|
||||
if (counter.Result.Type.IsBasic())
|
||||
return true;
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -16,19 +18,33 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
// Some implementations of this element allow seeking during gameplay playback.
|
||||
// Set a sane default of never handling input to override the behaviour provided by OverlayContainer.
|
||||
public override bool HandleNonPositionalInput => false;
|
||||
public override bool HandlePositionalInput => false;
|
||||
public override bool HandleNonPositionalInput => Interactive.Value;
|
||||
public override bool HandlePositionalInput => Interactive.Value;
|
||||
|
||||
protected override bool BlockScrollInput => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether interaction should be allowed (ie. seeking). If <c>false</c>, interaction controls will not be displayed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default, this will be automatically decided based on the gameplay state.
|
||||
/// </remarks>
|
||||
public readonly Bindable<bool> Interactive = new Bindable<bool>();
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
[Resolved]
|
||||
protected IGameplayClock GameplayClock { get; private set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DrawableRuleset? drawableRuleset { get; set; }
|
||||
[Resolved]
|
||||
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;
|
||||
|
||||
public IEnumerable<HitObject> Objects
|
||||
@ -58,15 +74,21 @@ namespace osu.Game.Screens.Play.HUD
|
||||
protected virtual void UpdateObjects(IEnumerable<HitObject> objects) { }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(DrawableRuleset? drawableRuleset, Player? player)
|
||||
{
|
||||
if (drawableRuleset != null)
|
||||
{
|
||||
if (player?.Configuration.AllowUserInteraction == true)
|
||||
((IBindable<bool>)Interactive).BindTo(drawableRuleset.HasReplayLoaded);
|
||||
|
||||
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()
|
||||
{
|
||||
base.Update();
|
||||
@ -74,9 +96,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (objects == null)
|
||||
return;
|
||||
|
||||
// The reference clock is used to accurately tell the playfield's time. This is obtained from the drawable ruleset.
|
||||
// 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;
|
||||
double currentTime = FrameStableClock.CurrentTime;
|
||||
|
||||
bool isInIntro = currentTime < FirstHitTime;
|
||||
|
||||
|
@ -10,6 +10,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
@ -27,13 +28,33 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private double songLength => endTime - startTime;
|
||||
|
||||
private const int margin = 10;
|
||||
public FontUsage Font
|
||||
{
|
||||
set
|
||||
{
|
||||
timeCurrent.Font = value;
|
||||
timeLeft.Font = value;
|
||||
progress.Font = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Colour4 TextColour
|
||||
{
|
||||
set
|
||||
{
|
||||
timeCurrent.Colour = value;
|
||||
timeLeft.Colour = value;
|
||||
progress.Colour = value;
|
||||
}
|
||||
}
|
||||
|
||||
public double StartTime
|
||||
{
|
||||
set => startTime = value;
|
||||
}
|
||||
|
||||
public bool ShowProgress { get; init; } = true;
|
||||
|
||||
public double EndTime
|
||||
{
|
||||
set => endTime = value;
|
||||
@ -76,6 +97,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Alpha = ShowProgress ? 1 : 0,
|
||||
Child = new UprightAspectMaintainingContainer
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
@ -99,15 +121,15 @@ namespace osu.Game.Screens.Play.HUD
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = new UprightAspectMaintainingContainer
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreRight,
|
||||
Anchor = Anchor.CentreRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Scaling = ScaleMode.Vertical,
|
||||
ScalingFactor = 0.5f,
|
||||
Child = timeLeft = new SizePreservingSpriteText
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreRight,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Colour = colours.BlueLighter,
|
||||
Font = OsuFont.Numeric,
|
||||
}
|
||||
@ -128,7 +150,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
if (currentPercent != previousPercent)
|
||||
{
|
||||
progress.Text = currentPercent.ToString() + @"%";
|
||||
progress.Text = currentPercent + @"%";
|
||||
previousPercent = currentPercent;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
// 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.
|
||||
@ -1070,7 +1072,7 @@ namespace osu.Game.Screens.Play
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
screenSuspension?.RemoveAndDisposeImmediately();
|
||||
failAnimationLayer?.RemoveFilters();
|
||||
failAnimationLayer?.Stop();
|
||||
|
||||
if (LoadedBeatmapSuccessfully)
|
||||
{
|
||||
|
@ -61,50 +61,75 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (!(other is CarouselBeatmapSet otherSet))
|
||||
return base.CompareTo(criteria, other);
|
||||
|
||||
int comparison = 0;
|
||||
|
||||
switch (criteria.Sort)
|
||||
{
|
||||
default:
|
||||
case SortMode.Artist:
|
||||
return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase);
|
||||
comparison = string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
|
||||
case SortMode.Title:
|
||||
return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase);
|
||||
comparison = string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
|
||||
case SortMode.Author:
|
||||
return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase);
|
||||
comparison = string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
|
||||
case SortMode.Source:
|
||||
return string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase);
|
||||
comparison = string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
|
||||
case SortMode.DateAdded:
|
||||
return otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
|
||||
comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
|
||||
break;
|
||||
|
||||
case SortMode.DateRanked:
|
||||
// Beatmaps which have no ranked date should already be filtered away in this mode.
|
||||
if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null)
|
||||
return 0;
|
||||
break;
|
||||
|
||||
return otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value);
|
||||
comparison = otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value);
|
||||
break;
|
||||
|
||||
case SortMode.LastPlayed:
|
||||
return -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
|
||||
comparison = -compareUsingAggregateMax(otherSet, b => (b.LastPlayed ?? DateTimeOffset.MinValue).ToUnixTimeSeconds());
|
||||
break;
|
||||
|
||||
case SortMode.BPM:
|
||||
return compareUsingAggregateMax(otherSet, b => b.BPM);
|
||||
comparison = compareUsingAggregateMax(otherSet, b => b.BPM);
|
||||
break;
|
||||
|
||||
case SortMode.Length:
|
||||
return compareUsingAggregateMax(otherSet, b => b.Length);
|
||||
comparison = compareUsingAggregateMax(otherSet, b => b.Length);
|
||||
break;
|
||||
|
||||
case SortMode.Difficulty:
|
||||
return compareUsingAggregateMax(otherSet, b => b.StarRating);
|
||||
comparison = compareUsingAggregateMax(otherSet, b => b.StarRating);
|
||||
break;
|
||||
|
||||
case SortMode.DateSubmitted:
|
||||
// Beatmaps which have no submitted date should already be filtered away in this mode.
|
||||
if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null)
|
||||
return 0;
|
||||
break;
|
||||
|
||||
return otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
|
||||
comparison = otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
|
||||
break;
|
||||
}
|
||||
|
||||
if (comparison != 0) return comparison;
|
||||
|
||||
// If the initial sort could not differentiate, attempt to use DateAdded to order sets in a stable fashion.
|
||||
// The directionality of this matches the current SortMode.DateAdded, but we may want to reconsider if that becomes a user decision (ie. asc / desc).
|
||||
comparison = otherSet.BeatmapSet.DateAdded.CompareTo(BeatmapSet.DateAdded);
|
||||
|
||||
if (comparison != 0) return comparison;
|
||||
|
||||
// If DateAdded fails to break the tie, fallback to our internal GUID for stability.
|
||||
// This basically means it's a stable random sort.
|
||||
return otherSet.BeatmapSet.ID.CompareTo(BeatmapSet.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -30,14 +30,16 @@ namespace osu.Game.Screens.Select.Details
|
||||
{
|
||||
public partial class AdvancedStats : Container
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
private OsuGameBase game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
private IBindable<RulesetInfo> gameRuleset;
|
||||
|
||||
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
|
||||
private readonly StatisticRow starDifficulty;
|
||||
@ -84,7 +86,13 @@ namespace osu.Game.Screens.Select.Details
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@ -142,7 +150,14 @@ namespace osu.Game.Screens.Select.Details
|
||||
|
||||
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();
|
||||
|
||||
@ -151,8 +166,8 @@ namespace osu.Game.Screens.Select.Details
|
||||
|
||||
starDifficultyCancellationSource = new CancellationTokenSource();
|
||||
|
||||
var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, null, starDifficultyCancellationSource.Token);
|
||||
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
|
||||
var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, gameRuleset.Value, null, starDifficultyCancellationSource.Token);
|
||||
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, gameRuleset.Value, mods.Value, starDifficultyCancellationSource.Token);
|
||||
|
||||
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);
|
||||
}), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
|
||||
}
|
||||
});
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
|
@ -108,6 +108,7 @@ namespace osu.Game.Skinning
|
||||
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
||||
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
|
||||
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
|
||||
var songProgress = container.OfType<ArgonSongProgress>().FirstOrDefault();
|
||||
|
||||
if (score != null)
|
||||
{
|
||||
@ -158,6 +159,12 @@ namespace osu.Game.Skinning
|
||||
// origin flipped to match scale above.
|
||||
hitError2.Origin = Anchor.CentreLeft;
|
||||
}
|
||||
|
||||
if (songProgress != null)
|
||||
{
|
||||
songProgress.Position = new Vector2(0, -10);
|
||||
songProgress.Scale = new Vector2(0.9f, 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
{
|
||||
@ -167,7 +174,7 @@ namespace osu.Game.Skinning
|
||||
new DefaultScoreCounter(),
|
||||
new DefaultAccuracyCounter(),
|
||||
new DefaultHealthDisplay(),
|
||||
new DefaultSongProgress(),
|
||||
new ArgonSongProgress(),
|
||||
new BarHitErrorMeter(),
|
||||
new BarHitErrorMeter(),
|
||||
new PerformancePointsCounter()
|
||||
|
@ -65,11 +65,14 @@ namespace osu.Game.Skinning
|
||||
default:
|
||||
|
||||
this.ScaleTo(0.6f).Then()
|
||||
.ScaleTo(1.1f, fade_in_length * 0.8f).Then()
|
||||
// this is actually correct to match stable; there were overlapping transforms.
|
||||
.ScaleTo(0.9f).Delay(fade_in_length * 0.2f)
|
||||
.ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then()
|
||||
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f);
|
||||
.ScaleTo(1.1f, fade_in_length * 0.8f).Then() // t = 0.8
|
||||
.Delay(fade_in_length * 0.2f) // t = 1.0
|
||||
.ScaleTo(0.9f, fade_in_length * 0.2f).Then() // t = 1.2
|
||||
|
||||
// stable dictates scale of 0.9->1 over time 1.0 to 1.4, but we are already at 1.2.
|
||||
// so we need to force the current value to be correct at 1.2 (0.95) then complete the
|
||||
// second half of the transform.
|
||||
.ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); // t = 1.4
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,10 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
private CircularProgress circularProgress = null!;
|
||||
|
||||
// Legacy song progress doesn't support interaction for now.
|
||||
public override bool HandleNonPositionalInput => false;
|
||||
public override bool HandlePositionalInput => false;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -56,16 +60,6 @@ namespace osu.Game.Skinning
|
||||
};
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.FadeOut(100);
|
||||
}
|
||||
|
||||
protected override void UpdateProgress(double progress, bool isIntro)
|
||||
{
|
||||
if (isIntro)
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Tests
|
||||
{
|
||||
Depth = 10,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}, AddInternal);
|
||||
}, Add);
|
||||
|
||||
// Have to construct this here, rather than in the constructor, because
|
||||
// we depend on some dependencies to be loaded within OsuGameBase.load().
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = 5,
|
||||
BorderThickness = 3,
|
||||
Masking = true,
|
||||
|
||||
Children = new Drawable[]
|
||||
@ -142,8 +142,15 @@ namespace osu.Game.Tests.Visual
|
||||
c.AutoSizeAxes = Axes.None;
|
||||
c.Size = Vector2.Zero;
|
||||
|
||||
c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None;
|
||||
c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None;
|
||||
if (autoSize)
|
||||
c.AutoSizeAxes = Axes.Both;
|
||||
else
|
||||
{
|
||||
c.RelativeSizeAxes = Axes.Both;
|
||||
c.Anchor = Anchor.Centre;
|
||||
c.Origin = Anchor.Centre;
|
||||
c.Size = new Vector2(0.97f);
|
||||
}
|
||||
}
|
||||
|
||||
outlineBox.Alpha = autoSize ? 1 : 0;
|
||||
|
@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<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="Sentry" Version="3.23.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
|
@ -16,6 +16,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1226.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.120.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user