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

Merge branch 'master' into revert-result-in-playfield

This commit is contained in:
Dean Herbert 2023-01-25 17:35:33 +09:00
commit f0406c34fd
105 changed files with 2907 additions and 564 deletions

View File

@ -121,21 +121,12 @@ jobs:
build-only-ios: build-only-ios:
name: Build only (iOS) name: Build only (iOS)
# change to macos-latest once GitHub finishes migrating all repositories to macOS 12. runs-on: macos-latest
runs-on: macos-12
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
# see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617
# remove once all workflow VMs use Xcode 14.1
- name: Set Xcode Version
shell: bash
run: |
sudo xcode-select -s "/Applications/Xcode_14.1.app"
echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV
- name: Install .NET 6.0.x - name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:

View File

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

View File

@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()), new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()),
new CatchModHidden(), new CatchModHidden(),
new CatchModFlashlight(), new CatchModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -0,0 +1,19 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModFadeIn : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[TestCase(0.5f)]
[TestCase(0.1f)]
[TestCase(0.7f)]
public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModFadeIn { Coverage = { Value = coverage } }, PassCondition = () => true });
}
}

View File

@ -0,0 +1,19 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModHidden : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[TestCase(0.5f)]
[TestCase(0.2f)]
[TestCase(0.8f)]
public void TestCoverage(float coverage) => CreateModTest(new ModTestData { Mod = new ManiaModHidden { Coverage = { Value = coverage } }, PassCondition = () => true });
}
}

View File

@ -245,6 +245,7 @@ namespace osu.Game.Rulesets.Mania
new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()), new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()),
new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()), new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()),
new ManiaModFlashlight(), new ManiaModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -18,5 +19,13 @@ namespace osu.Game.Rulesets.Mania.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll; protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
{
Precision = 0.1f,
MinValue = 0.1f,
MaxValue = 0.7f,
Default = 0.5f,
};
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
@ -13,6 +14,14 @@ namespace osu.Game.Rulesets.Mania.Mods
public override LocalisableString Description => @"Keys fade out before you hit them!"; public override LocalisableString Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
{
Precision = 0.1f,
MinValue = 0.2f,
MaxValue = 0.8f,
Default = 0.5f,
};
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;

View File

@ -3,8 +3,10 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Mania.Mods
/// </summary> /// </summary>
protected abstract CoverExpandDirection ExpandDirection { get; } protected abstract CoverExpandDirection ExpandDirection { get; }
[SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")]
public abstract BindableNumber<float> Coverage { get; }
public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset) public virtual void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
{ {
ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
@ -36,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{ {
c.RelativeSizeAxes = Axes.Both; c.RelativeSizeAxes = Axes.Both;
c.Direction = ExpandDirection; c.Direction = ExpandDirection;
c.Coverage = 0.5f; c.Coverage = Coverage.Value;
})); }));
} }
} }

View File

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

View File

@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
public Vector2 PathStartLocation => body.PathOffset; public Vector2 PathStartLocation => body.PathOffset;
/// <summary>
/// Offset in absolute (local) coordinates from the end of the curve.
/// </summary>
public Vector2 PathEndLocation => body.PathEndOffset;
public SliderBodyPiece() public SliderBodyPiece()
{ {
InternalChild = body = new ManualSliderBody InternalChild = body = new ManualSliderBody

View File

@ -409,6 +409,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset) public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); ?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
{
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
};
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;

View File

@ -0,0 +1,14 @@
// 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.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAccuracyChallenge : ModAccuracyChallenge
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
}
}

View File

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

View File

@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(), new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking() new OsuModStrictTracking(),
new OsuModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

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

View File

@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary> /// </summary>
public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]); public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]);
/// <summary>
/// Offset in absolute coordinates from the end of the curve.
/// </summary>
public virtual Vector2 PathEndOffset => path.PositionInBoundingBox(path.Vertices[^1]);
/// <summary> /// <summary>
/// Used to colour the path. /// Used to colour the path.
/// </summary> /// </summary>

View File

@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
public override Vector2 PathOffset => snakedPathOffset; public override Vector2 PathOffset => snakedPathOffset;
public override Vector2 PathEndOffset => snakedPathEndOffset;
/// <summary> /// <summary>
/// The top-left position of the path when fully snaked. /// The top-left position of the path when fully snaked.
/// </summary> /// </summary>
@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary> /// </summary>
private Vector2 snakedPathOffset; private Vector2 snakedPathOffset;
/// <summary>
/// The offset of the end of path from <see cref="snakedPosition"/> when fully snaked.
/// </summary>
private Vector2 snakedPathEndOffset;
private DrawableSlider drawableSlider = null!; private DrawableSlider drawableSlider = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]);
double lastSnakedStart = SnakedStart ?? 0; double lastSnakedStart = SnakedStart ?? 0;
double lastSnakedEnd = SnakedEnd ?? 0; double lastSnakedEnd = SnakedEnd ?? 0;

View File

@ -0,0 +1,108 @@
// 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;
using osuTK;
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);
}
// Required to handle touches outside of the playfield when screen scaling is enabled.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override void OnTouchMove(TouchMoveEvent e)
{
base.OnTouchMove(e);
handleTouchMovement(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
OsuAction action = trackedTouches.Any(t => t.Action == OsuAction.LeftButton)
? OsuAction.RightButton
: OsuAction.LeftButton;
// Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future.
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action);
trackedTouches.Add(new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null));
// Important to update position before triggering the pressed action.
handleTouchMovement(e);
if (shouldResultInAction)
osuInputManager.KeyBindingContainer.TriggerPressed(action);
return true;
}
private void handleTouchMovement(TouchEvent touchEvent)
{
// Movement should only be tracked for the most recent touch.
if (touchEvent.Touch.Source != trackedTouches.Last().Source)
return;
if (!osuInputManager.AllowUserCursorMovement)
return;
new MousePositionAbsoluteInput { Position = touchEvent.ScreenSpaceTouch.Position }.Apply(osuInputManager.CurrentState, osuInputManager);
}
protected override void OnTouchUp(TouchUpEvent e)
{
var tracked = trackedTouches.Single(t => t.Source == e.Touch.Source);
if (tracked.Action is OsuAction action)
osuInputManager.KeyBindingContainer.TriggerReleased(action);
trackedTouches.Remove(tracked);
base.OnTouchUp(e);
}
private class TrackedTouch
{
public readonly TouchSource Source;
public readonly OsuAction? Action;
public TrackedTouch(TouchSource source, OsuAction? action)
{
Source = source;
Action = action;
}
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
@ -9,30 +8,15 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>
{ {
private DrawableTaikoRuleset? drawableTaikoRuleset;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{ {
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false; drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield; var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true; playfield.ClassicHitTargetPosition.Value = true;
} }
public void Update(Playfield playfield)
{
Debug.Assert(drawableTaikoRuleset != null);
// Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = drawableTaikoRuleset.DrawHeight / 480;
drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
} }
} }

View File

@ -144,6 +144,7 @@ namespace osu.Game.Rulesets.Taiko
new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()), new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()),
new TaikoModHidden(), new TaikoModHidden(),
new TaikoModFlashlight(), new TaikoModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
Direction.Value = ScrollingDirection.Left; Direction.Value = ScrollingDirection.Left;
TimeRange.Value = 7000;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -60,6 +59,19 @@ namespace osu.Game.Rulesets.Taiko.UI
KeyBindingInputManager.Add(new DrumTouchInputArea()); KeyBindingInputManager.Add(new DrumTouchInputArea());
} }
protected override void Update()
{
base.Update();
// Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = DrawHeight / 480;
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();

View File

@ -41,10 +41,14 @@ namespace osu.Game.Tests.Skins
"Archives/modified-default-20220818.osk", "Archives/modified-default-20220818.osk",
// Covers longest combo counter // Covers longest combo counter
"Archives/modified-default-20221012.osk", "Archives/modified-default-20221012.osk",
// Covers Argon variant of song progress bar
"Archives/modified-argon-20221024.osk",
// Covers TextElement and BeatmapInfoDrawable // Covers TextElement and BeatmapInfoDrawable
"Archives/modified-default-20221102.osk", "Archives/modified-default-20221102.osk",
// Covers BPM counter. // Covers BPM counter.
"Archives/modified-default-20221205.osk" "Archives/modified-default-20221205.osk",
// Covers judgement counter.
"Archives/modified-default-20230117.osk"
}; };
/// <summary> /// <summary>

View File

@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Background
base.LoadComplete(); base.LoadComplete();
AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s); AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s);
AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s));
} }
} }
} }

View File

@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -42,6 +43,9 @@ namespace osu.Game.Tests.Visual.Editing
[Resolved] [Resolved]
private BeatmapManager beatmapManager { get; set; } = null!; private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty; private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
public override void SetUpSteps() public override void SetUpSteps()
@ -224,7 +228,8 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName && beatmap.DifficultyName == secondDifficultyName
&& set != null && set != null
&& set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified)); && set.PerformRead(s =>
s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
}); });
} }
@ -327,6 +332,56 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2)); AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2));
} }
[Test]
public void TestCopyDifficultyDoesNotChangeCollections()
{
string originalDifficultyName = Guid.NewGuid().ToString();
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName);
AddStep("save beatmap", () => Editor.Save());
string originalMd5 = string.Empty;
BeatmapCollection collection = null!;
AddStep("setup a collection with original beatmap", () =>
{
collection = new BeatmapCollection("test copy");
collection.BeatmapMD5Hashes.Add(originalMd5 = EditorBeatmap.BeatmapInfo.MD5Hash);
realm.Write(r =>
{
r.Add(collection);
});
});
AddAssert("collection contains original beatmap", () =>
!string.IsNullOrEmpty(originalMd5) && collection.BeatmapMD5Hashes.Contains(originalMd5));
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick());
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != originalDifficultyName;
});
AddStep("save without changes", () => Editor.Save());
AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash)
&& collection.BeatmapMD5Hashes.Contains(originalMd5));
AddStep("clean up collection", () =>
{
realm.Write(r =>
{
r.Remove(collection);
});
});
}
[Test] [Test]
public void TestCreateMultipleNewDifficultiesSucceeds() public void TestCreateMultipleNewDifficultiesSucceeds()
{ {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,182 @@
// 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.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.JudgementCounter;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneJudgementCounter : OsuTestScene
{
private ScoreProcessor scoreProcessor = null!;
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", () =>
{
var ruleset = CreateRuleset();
Debug.Assert(ruleset != null);
scoreProcessor = new ScoreProcessor(ruleset);
base.Content.Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(ScoreProcessor), scoreProcessor), (typeof(Ruleset), ruleset) },
Children = new Drawable[]
{
judgementTally = new JudgementTally(),
content = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(JudgementTally), judgementTally) },
}
},
};
});
protected override Ruleset CreateRuleset() => new ManiaRuleset();
private void applyOneJudgement(HitResult result)
{
lastJudgementResult.Value = new OsuJudgementResult(new HitObject
{
StartTime = iteration * 10000
}, new OsuJudgement())
{
Type = result,
};
scoreProcessor.ApplyResult(lastJudgementResult.Value);
iteration++;
}
[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);
}
[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);
}
[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);
}
[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);
AddStep("Hide judgement names", () => counterDisplay.ShowJudgementNames.Value = true);
AddWaitStep("wait some", 2);
AddAssert("Assert shown", () => counterDisplay.CounterFlow.Children.First().ResultName.Alpha == 1);
}
[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);
AddStep("Show normal judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Normal);
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
AddWaitStep("wait some", 2);
AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Last().Alpha == 1);
}
private int hiddenCount()
{
var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit);
return num.Result.ResultCount.Value;
}
private partial class TestJudgementCounterDisplay : JudgementCounterDisplay
{
public new FillFlowContainer<JudgementCounter> CounterFlow => base.CounterFlow;
public TestJudgementCounterDisplay()
{
Margin = new MarginPadding { Top = 100 };
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,54 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public partial class TestSceneLevelBadge : OsuTestScene
{
public TestSceneLevelBadge()
{
var levels = new List<UserStatistics.LevelInfo>();
for (int i = 0; i < 11; i++)
{
levels.Add(new UserStatistics.LevelInfo
{
Current = i * 10
});
}
levels.Add(new UserStatistics.LevelInfo { Current = 101 });
levels.Add(new UserStatistics.LevelInfo { Current = 105 });
levels.Add(new UserStatistics.LevelInfo { Current = 110 });
levels.Add(new UserStatistics.LevelInfo { Current = 115 });
levels.Add(new UserStatistics.LevelInfo { Current = 120 });
Children = new Drawable[]
{
new FillFlowContainer<LevelBadge>
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Full,
Spacing = new Vector2(5),
ChildrenEnumerable = levels.Select(level => new LevelBadge
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(60),
LevelInfo = { Value = level }
})
}
};
}
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Profile; using osu.Game.Overlays.Profile;
@ -19,6 +20,9 @@ namespace osu.Game.Tests.Visual.Online
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
private ProfileHeader header = null!; private ProfileHeader header = null!;
[SetUpSteps] [SetUpSteps]
@ -33,6 +37,22 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo)); AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo));
} }
[Test]
public void TestProfileCoverExpanded()
{
AddStep("Set cover to expanded", () => configManager.SetValue(OsuSetting.ProfileCoverExpanded, true));
AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo));
AddUntilStep("Cover is expanded", () => header.ChildrenOfType<UserCoverBackground>().Single().Height, () => Is.GreaterThan(0));
}
[Test]
public void TestProfileCoverCollapsed()
{
AddStep("Set cover to collapsed", () => configManager.SetValue(OsuSetting.ProfileCoverExpanded, false));
AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo));
AddUntilStep("Cover is collapsed", () => header.ChildrenOfType<UserCoverBackground>().Single().Height, () => Is.EqualTo(0));
}
[Test] [Test]
public void TestOnlineState() public void TestOnlineState()
{ {

View File

@ -82,13 +82,14 @@ namespace osu.Game.Tests.Visual.Online
{ {
Username = @"Somebody", Username = @"Somebody",
Id = 1, Id = 1,
CountryCode = CountryCode.Unknown, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
JoinDate = DateTimeOffset.Now.AddDays(-1), JoinDate = DateTimeOffset.Now.AddDays(-1),
LastVisit = DateTimeOffset.Now, LastVisit = DateTimeOffset.Now,
Groups = new[] Groups = new[]
{ {
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" }, new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "mania" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } } new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
}, },
ProfileOrder = new[] ProfileOrder = new[]
@ -142,7 +143,8 @@ namespace osu.Game.Tests.Visual.Online
{ {
Available = 10, Available = 10,
Total = 50 Total = 50
} },
SupportLevel = 2,
}; };
} }
} }

View File

@ -123,7 +123,7 @@ needs_cleanup: true
AddStep("Add absolute image", () => AddStep("Add absolute image", () =>
{ {
markdownContainer.CurrentPath = "https://dev.ppy.sh"; markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)"; markdownContainer.Text = "![intro](/wiki/images/Client/Interface/img/intro-screen.jpg)";
}); });
} }
@ -133,7 +133,7 @@ needs_cleanup: true
AddStep("Add relative image", () => AddStep("Add relative image", () =>
{ {
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/"; markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = "![intro](img/intro-screen.jpg)"; markdownContainer.Text = "![intro](../images/Client/Interface/img/intro-screen.jpg)";
}); });
} }
@ -145,7 +145,7 @@ needs_cleanup: true
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/"; markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = @"Line before image markdownContainer.Text = @"Line before image
![play menu](img/play-menu.jpg ""Main Menu in osu!"") ![play menu](../images/Client/Interface/img/play-menu.jpg ""Main Menu in osu!"")
Line after image"; Line after image";
}); });
@ -170,12 +170,12 @@ Line after image";
markdownContainer.Text = @" markdownContainer.Text = @"
| Image | Name | Effect | | Image | Name | Effect |
| :-: | :-: | :-- | | :-: | :-: | :-- |
| ![](/wiki/Skinning/Interface/img/hit300.png ""300"") | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. | | ![](/wiki/images/shared/judgement/osu!/hit300.png ""300"") | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
| ![](/wiki/Skinning/Interface/img/hit300g.png ""Geki"") | () Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. | | ![](/wiki/images/shared/judgement/osu!/hit300g.png ""Geki"") | () Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
| ![](/wiki/Skinning/Interface/img/hit100.png ""100"") | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. | | ![](/wiki/images/shared/judgement/osu!/hit100.png ""100"") | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
| ![](/wiki/Skinning/Interface/img/hit300k.png ""300 Katu"") ![](/wiki/Skinning/Interface/img/hit100k.png ""100 Katu"") | () Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. | | ![](/wiki/images/shared/judgement/osu!/hit300k.png ""300 Katu"") ![](/wiki/Skinning/Interface/img/hit100k.png ""100 Katu"") | () Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
| ![](/wiki/Skinning/Interface/img/hit50.png ""50"") | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. | | ![](/wiki/images/shared/judgement/osu!/hit50.png ""50"") | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
| ![](/wiki/Skinning/Interface/img/hit0.png ""Miss"") | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. | | ![](/wiki/images/shared/judgement/osu!/hit0.png ""Miss"") | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
"; ";
}); });
} }
@ -186,7 +186,7 @@ Line after image";
AddStep("Add image", () => AddStep("Add image", () =>
{ {
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/"; markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
markdownContainer.Text = "![](img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")"; markdownContainer.Text = "![](../images/Client/Program_files/img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")";
}); });
AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted); AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted);
@ -270,6 +270,30 @@ Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed
}); });
} }
[Test]
public void TestCodeSyntax()
{
AddStep("set content", () =>
{
markdownContainer.Text = @"
This is a paragraph containing `inline code` synatax.
Oh wow I do love the `WikiMarkdownContainer`, it is very cool!
This is a line before the fenced code block:
```csharp
public class WikiMarkdownContainer : MarkdownContainer
{
public WikiMarkdownContainer()
{
this.foo = bar;
}
}
```
This is a line after the fenced code block!
";
});
}
private partial class TestMarkdownContainer : WikiMarkdownContainer private partial class TestMarkdownContainer : WikiMarkdownContainer
{ {
public LinkInline Link; public LinkInline Link;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo); targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet; newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin); save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false);
workingBeatmapCache.Invalidate(targetBeatmapSet); workingBeatmapCache.Invalidate(targetBeatmapSet);
return GetWorkingBeatmap(newBeatmap.BeatmapInfo); return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
@ -280,77 +280,16 @@ namespace osu.Game.Beatmaps
public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap; public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
/// <summary> /// <summary>
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>. /// Saves an existing <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
/// </summary> /// </summary>
/// <remarks>
/// This method will also update any user beatmap collection hash references to the new post-saved hash.
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param> /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param> /// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param> /// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
{ save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true);
var setInfo = beatmapInfo.BeatmapSet;
Debug.Assert(setInfo != null);
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
// CopyTo() will undo such adjustments, while CopyFrom() will not.
beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
// ensure that two difficulties from the set don't point at the same beatmap file.
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
if (existingFileInfo != null)
DeleteFile(setInfo, existingFileInfo);
string oldMd5Hash = beatmapInfo.MD5Hash;
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash();
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
updateHashAndMarkDirty(setInfo);
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
setInfo.CopyChangesToRealm(liveBeatmapSet);
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
});
}
Debug.Assert(beatmapInfo.BeatmapSet != null);
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
{
var metadata = beatmapInfo.Metadata;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
}
}
public void DeleteAllVideos() public void DeleteAllVideos()
{ {
@ -460,6 +399,74 @@ namespace osu.Game.Beatmaps
setInfo.Status = BeatmapOnlineStatus.LocallyModified; setInfo.Status = BeatmapOnlineStatus.LocallyModified;
} }
private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, bool transferCollections)
{
var setInfo = beatmapInfo.BeatmapSet;
Debug.Assert(setInfo != null);
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
// CopyTo() will undo such adjustments, while CopyFrom() will not.
beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
// ensure that two difficulties from the set don't point at the same beatmap file.
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
if (existingFileInfo != null)
DeleteFile(setInfo, existingFileInfo);
string oldMd5Hash = beatmapInfo.MD5Hash;
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash();
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
updateHashAndMarkDirty(setInfo);
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
setInfo.CopyChangesToRealm(liveBeatmapSet);
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
});
}
Debug.Assert(beatmapInfo.BeatmapSet != null);
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
{
var metadata = beatmapInfo.Metadata;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
}
}
#region Implementation of ICanAcceptFiles #region Implementation of ICanAcceptFiles
public Task Import(params string[] paths) => beatmapImporter.Import(paths); public Task Import(params string[] paths) => beatmapImporter.Import(paths);

View File

@ -58,8 +58,12 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal); SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal);
SetDefault(OsuSetting.ProfileCoverExpanded, true);
SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full); SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full);
SetDefault(OsuSetting.SongSelectBackgroundBlur, true);
// Online settings // Online settings
SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Username, string.Empty);
SetDefault(OsuSetting.Token, string.Empty); SetDefault(OsuSetting.Token, string.Empty);
@ -339,6 +343,7 @@ namespace osu.Game.Configuration
ChatDisplayHeight, ChatDisplayHeight,
BeatmapListingCardSize, BeatmapListingCardSize,
ToolbarClockDisplayMode, ToolbarClockDisplayMode,
SongSelectBackgroundBlur,
Version, Version,
ShowFirstRunSetup, ShowFirstRunSetup,
ShowConvertedBeatmaps, ShowConvertedBeatmaps,
@ -375,5 +380,6 @@ namespace osu.Game.Configuration
LastProcessedMetadataId, LastProcessedMetadataId,
SafeAreaConsiderations, SafeAreaConsiderations,
ComboColourNormalisationAmount, ComboColourNormalisationAmount,
ProfileCoverExpanded,
} }
} }

View File

@ -31,12 +31,6 @@ namespace osu.Game.Graphics.Backgrounds
/// </summary> /// </summary>
private const float equilateral_triangle_ratio = 0.866f; private const float equilateral_triangle_ratio = 0.866f;
/// <summary>
/// How many screen-space pixels are smoothed over.
/// Same behavior as Sprite's EdgeSmoothness.
/// </summary>
private const float edge_smoothness = 1;
private Color4 colourLight = Color4.White; private Color4 colourLight = Color4.White;
public Color4 ColourLight public Color4 ColourLight
@ -115,7 +109,7 @@ namespace osu.Game.Graphics.Backgrounds
private void load(IRenderer renderer, ShaderManager shaders) private void load(IRenderer renderer, ShaderManager shaders)
{ {
texture = renderer.WhitePixel; texture = renderer.WhitePixel;
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE); shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder");
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -252,14 +246,17 @@ namespace osu.Game.Graphics.Backgrounds
private class TrianglesDrawNode : DrawNode private class TrianglesDrawNode : DrawNode
{ {
private float fill = 1f;
protected new Triangles Source => (Triangles)base.Source; protected new Triangles Source => (Triangles)base.Source;
private IShader shader; private IShader shader;
private Texture texture; private Texture texture;
private readonly List<TriangleParticle> parts = new List<TriangleParticle>(); private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
private Vector2 size; private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size;
private Vector2 size;
private IVertexBatch<TexturedVertex2D> vertexBatch; private IVertexBatch<TexturedVertex2D> vertexBatch;
public TrianglesDrawNode(Triangles source) public TrianglesDrawNode(Triangles source)
@ -290,29 +287,28 @@ namespace osu.Game.Graphics.Backgrounds
} }
shader.Bind(); shader.Bind();
shader.GetUniform<float>("thickness").UpdateValue(ref fill);
Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy;
foreach (TriangleParticle particle in parts) foreach (TriangleParticle particle in parts)
{ {
var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * equilateral_triangle_ratio); Vector2 relativeSize = Vector2.Divide(triangleSize * particle.Scale, size);
var triangle = new Triangle( Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f);
Vector2Extensions.Transform(particle.Position * size, DrawInfo.Matrix), Vector2 topRight = topLeft + new Vector2(relativeSize.X, 0f);
Vector2Extensions.Transform(particle.Position * size + offset, DrawInfo.Matrix), Vector2 bottomLeft = topLeft + new Vector2(0f, relativeSize.Y);
Vector2Extensions.Transform(particle.Position * size + new Vector2(-offset.X, offset.Y), DrawInfo.Matrix) Vector2 bottomRight = bottomLeft + new Vector2(relativeSize.X, 0f);
var drawQuad = new Quad(
Vector2Extensions.Transform(topLeft * size, DrawInfo.Matrix),
Vector2Extensions.Transform(topRight * size, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomLeft * size, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomRight * size, DrawInfo.Matrix)
); );
ColourInfo colourInfo = DrawColourInfo.Colour; ColourInfo colourInfo = DrawColourInfo.Colour;
colourInfo.ApplyChild(particle.Colour); colourInfo.ApplyChild(particle.Colour);
renderer.DrawTriangle( renderer.DrawQuad(texture, drawQuad, colourInfo, vertexAction: vertexBatch.AddAction);
texture,
triangle,
colourInfo,
null,
vertexBatch.AddAction,
Vector2.Divide(localInflationAmount, new Vector2(2 * offset.X, offset.Y)));
} }
shader.Unbind(); shader.Unbind();

View File

@ -5,6 +5,7 @@
using System; using System;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Colour;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -187,6 +188,41 @@ namespace osu.Game.Graphics
} }
} }
/// <summary>
/// Retrieves colour for a <see cref="RankingTier"/>.
/// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours
/// </summary>
public ColourInfo ForRankingTier(RankingTier tier)
{
switch (tier)
{
default:
case RankingTier.Iron:
return Color4Extensions.FromHex(@"BAB3AB");
case RankingTier.Bronze:
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"B88F7A"), Color4Extensions.FromHex(@"855C47"));
case RankingTier.Silver:
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"E0E0EB"), Color4Extensions.FromHex(@"A3A3C2"));
case RankingTier.Gold:
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"F0E4A8"), Color4Extensions.FromHex(@"E0C952"));
case RankingTier.Platinum:
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"A8F0EF"), Color4Extensions.FromHex(@"52E0DF"));
case RankingTier.Rhodium:
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"D9F8D3"), Color4Extensions.FromHex(@"A0CF96"));
case RankingTier.Radiant:
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"97DCFF"), Color4Extensions.FromHex(@"ED82FF"));
case RankingTier.Lustrous:
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"FFE600"), Color4Extensions.FromHex(@"ED82FF"));
}
}
/// <summary> /// <summary>
/// Returns a foreground text colour that is supposed to contrast well with /// Returns a foreground text colour that is supposed to contrast well with
/// the supplied <paramref name="backgroundColour"/>. /// the supplied <paramref name="backgroundColour"/>.

View File

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

View File

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

View File

@ -160,9 +160,12 @@ namespace osu.Game
protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method
/// <summary>
/// The current ruleset selection for the local user.
/// </summary>
[Cached] [Cached]
[Cached(typeof(IBindable<RulesetInfo>))] [Cached(typeof(IBindable<RulesetInfo>))]
protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>(); protected internal readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
/// <summary> /// <summary>
/// The current mod selection for the local user. /// The current mod selection for the local user.
@ -553,8 +556,8 @@ namespace osu.Game
case JoystickHandler jh: case JoystickHandler jh:
return new JoystickSettings(jh); return new JoystickSettings(jh);
case TouchHandler: case TouchHandler th:
return new InputSection.HandlerSection(handler); return new TouchSettings(th);
} }
} }

View File

@ -5,12 +5,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
@ -22,8 +24,6 @@ namespace osu.Game.Overlays
{ {
public partial class ChangelogOverlay : OnlineOverlay<ChangelogHeader> public partial class ChangelogOverlay : OnlineOverlay<ChangelogHeader>
{ {
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>(); public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>();
private List<APIChangelogBuild> builds; private List<APIChangelogBuild> builds;
@ -81,6 +81,8 @@ namespace osu.Game.Overlays
ArgumentNullException.ThrowIfNull(updateStream); ArgumentNullException.ThrowIfNull(updateStream);
ArgumentNullException.ThrowIfNull(version); ArgumentNullException.ThrowIfNull(version);
Show();
performAfterFetch(() => performAfterFetch(() =>
{ {
var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream) var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream)
@ -89,8 +91,6 @@ namespace osu.Game.Overlays
if (build != null) if (build != null)
ShowBuild(build); ShowBuild(build);
}); });
Show();
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
@ -127,11 +127,16 @@ namespace osu.Game.Overlays
private Task initialFetchTask; private Task initialFetchTask;
private void performAfterFetch(Action action) => Schedule(() => private void performAfterFetch(Action action)
{ {
fetchListing()?.ContinueWith(_ => Debug.Assert(State.Value == Visibility.Visible);
Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
}); Schedule(() =>
{
fetchListing()?.ContinueWith(_ =>
Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
});
}
private Task fetchListing() private Task fetchListing()
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using Humanizer; using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -15,7 +13,12 @@ namespace osu.Game.Overlays.Comments.Buttons
public ShowRepliesButton(int count) public ShowRepliesButton(int count)
{ {
Text = "reply".ToQuantity(count); Count = count;
}
public int Count
{
set => Text = "reply".ToQuantity(value);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -12,6 +13,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components namespace osu.Game.Overlays.Profile.Header.Components
@ -23,6 +25,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
public LocalisableString TooltipText { get; private set; } public LocalisableString TooltipText { get; private set; }
private OsuSpriteText levelText = null!; private OsuSpriteText levelText = null!;
private Sprite sprite = null!;
[Resolved]
private OsuColour osuColour { get; set; } = null!;
public LevelBadge() public LevelBadge()
{ {
@ -34,7 +40,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Sprite sprite = new Sprite
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Texture = textures.Get("Profile/levelbadge"), Texture = textures.Get("Profile/levelbadge"),
@ -58,9 +64,34 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateLevel(UserStatistics.LevelInfo? levelInfo) private void updateLevel(UserStatistics.LevelInfo? levelInfo)
{ {
string level = levelInfo?.Current.ToString() ?? "0"; int level = levelInfo?.Current ?? 0;
levelText.Text = level;
TooltipText = UsersStrings.ShowStatsLevel(level); levelText.Text = level.ToString();
TooltipText = UsersStrings.ShowStatsLevel(level.ToString());
sprite.Colour = mapLevelToTierColour(level);
}
private ColourInfo mapLevelToTierColour(int level)
{
var tier = RankingTier.Iron;
if (level > 0)
{
tier = (RankingTier)(level / 20);
}
if (level >= 105)
{
tier = RankingTier.Radiant;
}
if (level >= 110)
{
tier = RankingTier.Lustrous;
}
return osuColour.ForRankingTier(tier);
} }
} }
} }

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -15,11 +14,11 @@ using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components namespace osu.Game.Overlays.Profile.Header.Components
{ {
public partial class ExpandDetailsButton : ProfileHeaderButton public partial class ToggleCoverButton : ProfileHeaderButton
{ {
public readonly BindableBool DetailsVisible = new BindableBool(); public readonly BindableBool CoverExpanded = new BindableBool(true);
public override LocalisableString TooltipText => DetailsVisible.Value ? CommonStrings.ButtonsCollapse : CommonStrings.ButtonsExpand; public override LocalisableString TooltipText => CoverExpanded.Value ? UsersStrings.ShowCoverTo0 : UsersStrings.ShowCoverTo1;
private SpriteIcon icon = null!; private SpriteIcon icon = null!;
private Sample? sampleOpen; private Sample? sampleOpen;
@ -27,12 +26,12 @@ namespace osu.Game.Overlays.Profile.Header.Components
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds();
public ExpandDetailsButton() public ToggleCoverButton()
{ {
Action = () => Action = () =>
{ {
DetailsVisible.Toggle(); CoverExpanded.Toggle();
(DetailsVisible.Value ? sampleOpen : sampleClose)?.Play(); (CoverExpanded.Value ? sampleOpen : sampleClose)?.Play();
}; };
} }
@ -40,19 +39,21 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void load(OverlayColourProvider colourProvider, AudioManager audio) private void load(OverlayColourProvider colourProvider, AudioManager audio)
{ {
IdleColour = colourProvider.Background2; IdleColour = colourProvider.Background2;
HoverColour = colourProvider.Background2.Lighten(0.2f); HoverColour = colourProvider.Background1;
sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
sampleClose = audio.Samples.Get(@"UI/dropdown-close"); sampleClose = audio.Samples.Get(@"UI/dropdown-close");
AutoSizeAxes = Axes.None;
Size = new Vector2(30);
Child = icon = new SpriteIcon Child = icon = new SpriteIcon
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(20, 12) Size = new Vector2(10.5f, 12)
}; };
DetailsVisible.BindValueChanged(visible => updateState(visible.NewValue), true); CoverExpanded.BindValueChanged(visible => updateState(visible.NewValue), true);
} }
private void updateState(bool detailsVisible) => icon.Icon = detailsVisible ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown; private void updateState(bool detailsVisible) => icon.Icon = detailsVisible ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown;

View File

@ -7,13 +7,16 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Users;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK; using osuTK;
@ -21,13 +24,15 @@ namespace osu.Game.Overlays.Profile.Header
{ {
public partial class TopHeaderContainer : CompositeDrawable public partial class TopHeaderContainer : CompositeDrawable
{ {
private const float avatar_size = 110; private const float content_height = 65;
private const float vertical_padding = 10;
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; private IAPIProvider api { get; set; } = null!;
private UserCoverBackground cover = null!;
private SupporterIcon supporterTag = null!; private SupporterIcon supporterTag = null!;
private UpdateableAvatar avatar = null!; private UpdateableAvatar avatar = null!;
private OsuSpriteText usernameText = null!; private OsuSpriteText usernameText = null!;
@ -36,11 +41,19 @@ namespace osu.Game.Overlays.Profile.Header
private UpdateableFlag userFlag = null!; private UpdateableFlag userFlag = null!;
private OsuSpriteText userCountryText = null!; private OsuSpriteText userCountryText = null!;
private GroupBadgeFlow groupBadgeFlow = null!; private GroupBadgeFlow groupBadgeFlow = null!;
private ToggleCoverButton coverToggle = null!;
private Bindable<bool> coverExpanded = null!;
private FillFlowContainer flow = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider, OsuConfigManager configManager)
{ {
Height = 150; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
coverExpanded = configManager.GetBindable<bool>(OsuSetting.ProfileCoverExpanded);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -51,124 +64,147 @@ namespace osu.Game.Overlays.Profile.Header
}, },
new FillFlowContainer new FillFlowContainer
{ {
Direction = FillDirection.Horizontal, RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }, AutoSizeAxes = Axes.Y,
Height = avatar_size, Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.X,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[] Children = new Drawable[]
{ {
avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false) cover = new ProfileCoverBackground
{ {
Size = new Vector2(avatar_size), RelativeSizeAxes = Axes.X,
Masking = true,
CornerRadius = avatar_size * 0.25f,
}, },
new OsuContextMenuContainer new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.Y,
Child = new Container Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Y, flow = new FillFlowContainer
AutoSizeAxes = Axes.X,
Padding = new MarginPadding { Left = 10 },
Children = new Drawable[]
{ {
new FillFlowContainer Direction = FillDirection.Horizontal,
Padding = new MarginPadding
{ {
AutoSizeAxes = Axes.Both, Left = UserProfileOverlay.CONTENT_X_MARGIN,
Direction = FillDirection.Vertical, Vertical = vertical_padding
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
usernameText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
},
openUserExternally = new ExternalLinkButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
groupBadgeFlow = new GroupBadgeFlow
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
},
titleText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
},
}
}, },
new FillFlowContainer Height = content_height + 2 * vertical_padding,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{ {
Origin = Anchor.BottomLeft, avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false)
Anchor = Anchor.BottomLeft,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
supporterTag = new SupporterIcon Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{ {
Height = 20, Type = EdgeEffectType.Shadow,
Margin = new MarginPadding { Top = 5 } Offset = new Vector2(0, 1),
}, Radius = 3,
new Box Colour = Colour4.Black.Opacity(0.25f),
{ }
RelativeSizeAxes = Axes.X, },
Height = 1.5f, new OsuContextMenuContainer
Margin = new MarginPadding { Top = 10 }, {
Colour = colourProvider.Light1, Anchor = Anchor.BottomLeft,
}, Origin = Anchor.BottomLeft,
new FillFlowContainer RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Child = new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 5 }, Direction = FillDirection.Vertical,
Direction = FillDirection.Horizontal, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[] Children = new Drawable[]
{ {
userFlag = new UpdateableFlag new FillFlowContainer
{ {
Size = new Vector2(28, 20), AutoSizeAxes = Axes.Both,
ShowPlaceholderOnUnknown = false, Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
usernameText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
},
supporterTag = new SupporterIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 15,
},
openUserExternally = new ExternalLinkButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
groupBadgeFlow = new GroupBadgeFlow
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
}, },
userCountryText = new OsuSpriteText titleText = new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular), Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
Margin = new MarginPadding { Left = 10 }, Margin = new MarginPadding { Bottom = 5 }
Origin = Anchor.CentreLeft, },
Anchor = Anchor.CentreLeft, new FillFlowContainer
Colour = colourProvider.Light1, {
} AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
userFlag = new UpdateableFlag
{
Size = new Vector2(28, 20),
ShowPlaceholderOnUnknown = false,
},
userCountryText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
Margin = new MarginPadding { Left = 5 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
}
}
},
} }
}, },
} },
} }
},
coverToggle = new ToggleCoverButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 10 },
CoverExpanded = { BindTarget = coverExpanded }
} }
} },
} },
} },
}, },
}; };
}
User.BindValueChanged(user => updateUser(user.NewValue)); protected override void LoadComplete()
{
base.LoadComplete();
User.BindValueChanged(user => updateUser(user.NewValue), true);
coverExpanded.BindValueChanged(_ => updateCoverState(), true);
FinishTransforms(true);
} }
private void updateUser(UserProfileData? data) private void updateUser(UserProfileData? data)
{ {
var user = data?.User; var user = data?.User;
cover.User = user;
avatar.User = user; avatar.User = user;
usernameText.Text = user?.Username ?? string.Empty; usernameText.Text = user?.Username ?? string.Empty;
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
@ -179,5 +215,27 @@ namespace osu.Game.Overlays.Profile.Header
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
groupBadgeFlow.User.Value = user; groupBadgeFlow.User.Value = user;
} }
private void updateCoverState()
{
const float transition_duration = 500;
bool expanded = coverToggle.CoverExpanded.Value;
cover.ResizeHeightTo(expanded ? 250 : 0, transition_duration, Easing.OutQuint);
avatar.ResizeTo(new Vector2(expanded ? 120 : content_height), transition_duration, Easing.OutQuint);
avatar.TransformTo(nameof(avatar.CornerRadius), expanded ? 40f : 20f, transition_duration, Easing.OutQuint);
flow.TransformTo(nameof(flow.Spacing), new Vector2(expanded ? 20f : 10f), transition_duration, Easing.OutQuint);
}
private partial class ProfileCoverBackground : UserCoverBackground
{
protected override double LoadDelay => 0;
public ProfileCoverBackground()
{
Masking = true;
}
}
} }
} }

View File

@ -3,23 +3,17 @@
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Overlays.Profile.Header; using osu.Game.Overlays.Profile.Header;
using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile namespace osu.Game.Overlays.Profile
{ {
public partial class ProfileHeader : TabControlOverlayHeader<LocalisableString> public partial class ProfileHeader : TabControlOverlayHeader<LocalisableString>
{ {
private UserCoverBackground coverContainer = null!;
public Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); public Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
private CentreHeaderContainer centreHeaderContainer; private CentreHeaderContainer centreHeaderContainer;
@ -29,8 +23,6 @@ namespace osu.Game.Overlays.Profile
{ {
ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN; ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN;
User.ValueChanged += e => updateDisplay(e.NewValue);
TabControl.AddItem(LayoutStrings.HeaderUsersShow); TabControl.AddItem(LayoutStrings.HeaderUsersShow);
// todo: pending implementation. // todo: pending implementation.
@ -41,25 +33,7 @@ namespace osu.Game.Overlays.Profile
Debug.Assert(detailHeaderContainer != null); Debug.Assert(detailHeaderContainer != null);
} }
protected override Drawable CreateBackground() => protected override Drawable CreateBackground() => Empty();
new Container
{
RelativeSizeAxes = Axes.X,
Height = 150,
Masking = true,
Children = new Drawable[]
{
coverContainer = new ProfileCoverBackground
{
RelativeSizeAxes = Axes.Both,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("222").Opacity(0.8f), Color4Extensions.FromHex("222").Opacity(0.2f))
},
}
};
protected override Drawable CreateContent() => new FillFlowContainer protected override Drawable CreateContent() => new FillFlowContainer
{ {
@ -103,8 +77,6 @@ namespace osu.Game.Overlays.Profile
User = { BindTarget = User } User = { BindTarget = User }
}; };
private void updateDisplay(UserProfileData? user) => coverContainer.User = user?.User;
private partial class ProfileHeaderTitle : OverlayTitle private partial class ProfileHeaderTitle : OverlayTitle
{ {
public ProfileHeaderTitle() public ProfileHeaderTitle()
@ -113,10 +85,5 @@ namespace osu.Game.Overlays.Profile
IconTexture = "Icons/Hexacons/profile"; IconTexture = "Icons/Hexacons/profile";
} }
} }
private partial class ProfileCoverBackground : UserCoverBackground
{
protected override double LoadDelay => 0;
}
} }
} }

View File

@ -35,6 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = SkinSettingsStrings.GameplayCursorDuringTouch, LabelText = SkinSettingsStrings.GameplayCursorDuringTouch,
Keywords = new[] { @"touchscreen" },
Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch) Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch)
}, },
}; };

View File

@ -70,6 +70,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
Add(new SettingsButton Add(new SettingsButton
{ {
Text = GeneralSettingsStrings.OpenOsuFolder, Text = GeneralSettingsStrings.OpenOsuFolder,
Keywords = new[] { @"logs", @"files", @"access", "directory" },
Action = () => storage.PresentExternally(), Action = () => storage.PresentExternally(),
}); });

View File

@ -34,6 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections
new SettingsButton new SettingsButton
{ {
Text = GeneralSettingsStrings.RunSetupWizard, Text = GeneralSettingsStrings.RunSetupWizard,
Keywords = new[] { @"first run", @"initial", @"getting started" },
TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription, TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription,
Action = () => firstRunSetupOverlay?.Show(), Action = () => firstRunSetupOverlay?.Show(),
}, },

View File

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

View File

@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings; protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings;
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" }); public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys" });
public BindingSettings(KeyBindingPanel keyConfig) public BindingSettings(KeyBindingPanel keyConfig)
{ {

View File

@ -0,0 +1,40 @@
// 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.Graphics;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Input
{
public partial class TouchSettings : SettingsSubsection
{
private readonly TouchHandler handler;
public TouchSettings(TouchHandler handler)
{
this.handler = handler;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = CommonStrings.Enabled,
Current = handler.Enabled
},
};
}
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"touchscreen" });
protected override LocalisableString Header => handler.Description;
}
}

View File

@ -42,6 +42,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelText = UserInterfaceStrings.ModSelectHotkeyStyle, LabelText = UserInterfaceStrings.ModSelectHotkeyStyle,
Current = config.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle), Current = config.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle),
ClassicDefault = ModSelectHotkeyStyle.Classic ClassicDefault = ModSelectHotkeyStyle.Classic
},
new SettingsCheckbox
{
LabelText = GameplaySettingsStrings.BackgroundBlur,
Current = config.GetBindable<bool>(OsuSetting.SongSelectBackgroundBlur),
ClassicDefault = false,
} }
}; };
} }

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Settings
public LocalisableString TooltipText { get; set; } public LocalisableString TooltipText { get; set; }
public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>();
public BindableBool CanBeShown { get; } = new BindableBool(true); public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown; IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
@ -30,9 +32,13 @@ namespace osu.Game.Overlays.Settings
get get
{ {
if (TooltipText != default) if (TooltipText != default)
return base.FilterTerms.Append(TooltipText); yield return TooltipText;
return base.FilterTerms; foreach (string s in Keywords)
yield return s;
foreach (LocalisableString s in base.FilterTerms)
yield return s;
} }
} }
} }

View File

@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Settings
Text = game.Name, Text = game.Name,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
}, },
new BuildDisplay(game.Version, DebugUtils.IsDebugBuild) new BuildDisplay(game.Version)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
@ -81,15 +81,13 @@ namespace osu.Game.Overlays.Settings
private partial class BuildDisplay : OsuAnimatedButton private partial class BuildDisplay : OsuAnimatedButton
{ {
private readonly string version; private readonly string version;
private readonly bool isDebug;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
public BuildDisplay(string version, bool isDebug) public BuildDisplay(string version)
{ {
this.version = version; this.version = version;
this.isDebug = isDebug;
Content.RelativeSizeAxes = Axes.Y; Content.RelativeSizeAxes = Axes.Y;
Content.AutoSizeAxes = AutoSizeAxes = Axes.X; Content.AutoSizeAxes = AutoSizeAxes = Axes.X;
@ -99,8 +97,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(ChangelogOverlay changelog) private void load(ChangelogOverlay changelog)
{ {
if (!isDebug) Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
Add(new OsuSpriteText Add(new OsuSpriteText
{ {
@ -110,7 +107,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Padding = new MarginPadding(5), Padding = new MarginPadding(5),
Colour = isDebug ? colours.Red : Color4.White, Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,
}); });
} }
} }

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System; using System;
using System.Linq;
using osu.Framework; using osu.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -125,10 +126,21 @@ namespace osu.Game.Rulesets.Edit
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>(); public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
/// <summary> /// <summary>
/// The screen-space point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag. /// The screen-space main point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag.
/// </summary> /// </summary>
public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre; public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
/// <summary>
/// Any points that should be used for snapping purposes in addition to <see cref="ScreenSpaceSelectionPoint"/>. Exposed via <see cref="ScreenSpaceSnapPoints"/>.
/// </summary>
protected virtual Vector2[] ScreenSpaceAdditionalNodes => Array.Empty<Vector2>();
/// <summary>
/// The screen-space collection of base points on this <see cref="HitObjectSelectionBlueprint"/> that other objects can be snapped to.
/// The first element of this collection is <see cref="ScreenSpaceSelectionPoint"/>
/// </summary>
public Vector2[] ScreenSpaceSnapPoints => ScreenSpaceAdditionalNodes.Prepend(ScreenSpaceSelectionPoint).ToArray();
/// <summary> /// <summary>
/// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections. /// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections.
/// </summary> /// </summary>

View File

@ -0,0 +1,80 @@
// 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.Globalization;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Judgements;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
public class ModAccuracyChallenge : ModFailCondition, IApplicableToScoreProcessor
{
public override string Name => "Accuracy Challenge";
public override string Acronym => "AC";
public override LocalisableString Description => "Fail if your accuracy drops too low!";
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModEasyWithExtraLives), typeof(ModPerfect) }).ToArray();
public override bool RequiresConfiguration => false;
public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo));
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsSlider<double, PercentSlider>))]
public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble
{
MinValue = 0.60,
MaxValue = 0.99,
Precision = 0.01,
Default = 0.9,
Value = 0.9,
};
private ScoreProcessor scoreProcessor = null!;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) => this.scoreProcessor = scoreProcessor;
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
{
if (!result.Type.AffectsAccuracy())
return false;
return getAccuracyWithImminentResultAdded(result) < MinimumAccuracy.Value;
}
private double getAccuracyWithImminentResultAdded(JudgementResult result)
{
var score = new ScoreInfo { Ruleset = scoreProcessor.Ruleset.RulesetInfo };
// This is super ugly, but if we don't do it this way we will not have the most recent result added to the accuracy value.
// Hopefully we can improve this in the future.
scoreProcessor.PopulateScore(score);
score.Statistics[result.Type]++;
return scoreProcessor.ComputeAccuracy(score);
}
}
public partial class PercentSlider : OsuSliderBar<double>
{
public PercentSlider()
{
DisplayAsPercentage = true;
}
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Humanizer; using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -19,6 +21,7 @@ namespace osu.Game.Rulesets.Mods
}; };
public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray();
private int retries; private int retries;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override LocalisableString Description => "SS or quit."; public override LocalisableString Description => "SS or quit.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray();
protected ModPerfect() protected ModPerfect()
{ {

View File

@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
protected virtual double ClassicScoreMultiplier => 36; protected virtual double ClassicScoreMultiplier => 36;
private readonly Ruleset ruleset; /// <summary>
/// The ruleset this score processor is valid for.
/// </summary>
public readonly Ruleset Ruleset;
private readonly double accuracyPortion; private readonly double accuracyPortion;
private readonly double comboPortion; private readonly double comboPortion;
@ -145,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring
public ScoreProcessor(Ruleset ruleset) public ScoreProcessor(Ruleset ruleset)
{ {
this.ruleset = ruleset; Ruleset = ruleset;
accuracyPortion = DefaultAccuracyPortion; accuracyPortion = DefaultAccuracyPortion;
comboPortion = DefaultComboPortion; comboPortion = DefaultComboPortion;
@ -291,8 +295,8 @@ namespace osu.Game.Rulesets.Scoring
[Pure] [Pure]
public double ComputeAccuracy(ScoreInfo scoreInfo) public double ComputeAccuracy(ScoreInfo scoreInfo)
{ {
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
// We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap. // We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
@ -312,8 +316,8 @@ namespace osu.Game.Rulesets.Scoring
[Pure] [Pure]
public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
{ {
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
extractScoringValues(scoreInfo, out var current, out var maximum); extractScoringValues(scoreInfo, out var current, out var maximum);
@ -552,7 +556,7 @@ namespace osu.Game.Rulesets.Scoring
break; break;
default: default:
maxResult = maxBasicResult ??= ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result; maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
break; break;
} }

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Scoring
{
public enum RankingTier
{
Iron,
Bronze,
Silver,
Gold,
Platinum,
Rhodium,
Radiant,
Lustrous
}
}

View File

@ -439,7 +439,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Movement #region Selection Movement
private Vector2[] movementBlueprintOriginalPositions; private Vector2[][] movementBlueprintsOriginalPositions;
private SelectionBlueprint<T>[] movementBlueprints; private SelectionBlueprint<T>[] movementBlueprints;
private bool isDraggingBlueprint; private bool isDraggingBlueprint;
@ -459,7 +459,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray();
return true; return true;
} }
@ -480,26 +480,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null) if (movementBlueprints == null)
return false; return false;
Debug.Assert(movementBlueprintOriginalPositions != null); Debug.Assert(movementBlueprintsOriginalPositions != null);
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
if (snapProvider != null) if (snapProvider != null)
{ {
// check for positional snap for every object in selection (for things like object-object snapping) for (int i = 0; i < movementBlueprints.Length; i++)
for (int i = 0; i < movementBlueprintOriginalPositions.Length; i++)
{ {
Vector2 originalPosition = movementBlueprintOriginalPositions[i]; if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i]))
var testPosition = originalPosition + distanceTravelled;
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
if (positionalResult.ScreenSpacePosition == testPosition) continue;
var delta = positionalResult.ScreenSpacePosition - movementBlueprints[i].ScreenSpaceSelectionPoint;
// attempt to move the objects, and abort any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints[i], delta)))
return true; return true;
} }
} }
@ -508,7 +497,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// item in the selection. // item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition. // The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled;
// Retrieve a snapped position. // Retrieve a snapped position.
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
@ -521,6 +510,36 @@ namespace osu.Game.Screens.Edit.Compose.Components
return ApplySnapResult(movementBlueprints, result); return ApplySnapResult(movementBlueprints, result);
} }
/// <summary>
/// Check for positional snap for given blueprint.
/// </summary>
/// <param name="blueprint">The blueprint to check for snapping.</param>
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
/// <returns>Whether an object to snap to was found.</returns>
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<T> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
{
var currentPositions = blueprint.ScreenSpaceSnapPoints;
for (int i = 0; i < originalPositions.Length; i++)
{
Vector2 originalPosition = originalPositions[i];
var testPosition = originalPosition + distanceTravelled;
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
if (positionalResult.ScreenSpacePosition == testPosition) continue;
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
// attempt to move the objects, and abort any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprint, delta)))
return true;
}
return false;
}
protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) => protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) =>
SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint)); SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint));
@ -533,7 +552,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null) if (movementBlueprints == null)
return false; return false;
movementBlueprintOriginalPositions = null; movementBlueprintsOriginalPositions = null;
movementBlueprints = null; movementBlueprints = null;
return true; return true;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgressGraph : SegmentedGraph<int>
{
private IEnumerable<HitObject>? objects;
public IEnumerable<HitObject> Objects
{
set
{
objects = value;
const int granularity = 200;
int[] values = new int[granularity];
if (!objects.Any())
return;
double firstHit = objects.First().StartTime;
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0)
lastHit = objects.Last().StartTime;
double interval = (lastHit - firstHit + 1) / granularity;
foreach (var h in objects)
{
double endTime = h.GetEndTime();
Debug.Assert(endTime >= h.StartTime);
int startRange = (int)((h.StartTime - firstHit) / interval);
int endRange = (int)((endTime - firstHit) / interval);
for (int i = startRange; i <= endRange; i++)
values[i]++;
}
Values = values;
}
}
public ArgonSongProgressGraph()
: base(5)
{
var colours = new List<Colour4>();
for (int i = 0; i < 5; i++)
colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f));
TierColours = colours;
}
}
}

View File

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

View File

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

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -15,17 +13,17 @@ using osu.Framework.Threading;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class SongProgressBar : SliderBar<double> public partial class DefaultSongProgressBar : SliderBar<double>
{ {
public Action<double> OnSeek; /// <summary>
/// Action which is invoked when a seek is requested, with the proposed millisecond value for the seek operation.
/// </summary>
public Action<double>? OnSeek { get; set; }
private readonly Box fill; /// <summary>
private readonly Container handleBase; /// Whether the progress bar should allow interaction, ie. to perform seek operations.
private readonly Container handleContainer; /// </summary>
public bool Interactive
private bool showHandle;
public bool ShowHandle
{ {
get => showHandle; get => showHandle;
set set
@ -59,7 +57,13 @@ namespace osu.Game.Screens.Play.HUD
set => CurrentNumber.Value = value; set => CurrentNumber.Value = value;
} }
public SongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize) private readonly Box fill;
private readonly Container handleBase;
private readonly Container handleContainer;
private bool showHandle;
public DefaultSongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
{ {
CurrentNumber.MinValue = 0; CurrentNumber.MinValue = 0;
CurrentNumber.MaxValue = 1; CurrentNumber.MaxValue = 1;
@ -142,7 +146,7 @@ namespace osu.Game.Screens.Play.HUD
handleBase.X = newX; handleBase.X = newX;
} }
private ScheduledDelegate scheduledSeek; private ScheduledDelegate? scheduledSeek;
protected override void OnUserChange(double value) protected override void OnUserChange(double value)
{ {

View File

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

View File

@ -0,0 +1,82 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD.JudgementCounter
{
public partial class JudgementCounter : VisibilityContainer
{
public BindableBool ShowName = new BindableBool();
public Bindable<FillDirection> Direction = new Bindable<FillDirection>();
public readonly JudgementTally.JudgementCount Result;
public JudgementCounter(JudgementTally.JudgementCount result) => Result = result;
public OsuSpriteText ResultName = null!;
private FillFlowContainer flowContainer = null!;
private JudgementRollingCounter counter = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours, IBindable<RulesetInfo> ruleset)
{
AutoSizeAxes = Axes.Both;
InternalChild = flowContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
counter = new JudgementRollingCounter
{
Current = Result.ResultCount
},
ResultName = new OsuSpriteText
{
Alpha = 0,
Font = OsuFont.Numeric.With(size: 8),
Text = ruleset.Value.CreateInstance().GetDisplayNameForHitResult(Result.Type)
}
}
};
var result = Result.Type;
Colour = result.IsBasic() ? colours.ForHitResult(Result.Type) : !result.IsBonus() ? colours.PurpleLight : colours.PurpleLighter;
}
protected override void LoadComplete()
{
ShowName.BindValueChanged(value =>
ResultName.FadeTo(value.NewValue ? 1 : 0, JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint), true);
Direction.BindValueChanged(direction =>
{
flowContainer.Direction = direction.NewValue;
changeAnchor(direction.NewValue == FillDirection.Vertical ? Anchor.TopLeft : Anchor.BottomLeft);
void changeAnchor(Anchor anchor) => counter.Anchor = ResultName.Anchor = counter.Origin = ResultName.Origin = anchor;
}, true);
base.LoadComplete();
}
protected override void PopIn() => this.FadeIn(JudgementCounterDisplay.TRANSFORM_DURATION, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(100);
private sealed partial class JudgementRollingCounter : RollingCounter<int>
{
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true, size: 16));
}
}
}

View File

@ -0,0 +1,138 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD.JudgementCounter
{
public partial class JudgementCounterDisplay : CompositeDrawable, ISkinnableDrawable
{
public const int TRANSFORM_DURATION = 250;
public bool UsesFixedAnchor { get; set; }
[SettingSource("Display mode")]
public Bindable<DisplayMode> Mode { get; set; } = new Bindable<DisplayMode>();
[SettingSource("Counter direction")]
public Bindable<Direction> FlowDirection { get; set; } = new Bindable<Direction>();
[SettingSource("Show judgement names")]
public BindableBool ShowJudgementNames { get; set; } = new BindableBool(true);
[SettingSource("Show max judgement")]
public BindableBool ShowMaxJudgement { get; set; } = new BindableBool(true);
[Resolved]
private JudgementTally tally { get; set; } = null!;
protected FillFlowContainer<JudgementCounter> CounterFlow = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChild = CounterFlow = new FillFlowContainer<JudgementCounter>
{
Direction = getFillDirection(FlowDirection.Value),
Spacing = new Vector2(10),
AutoSizeAxes = Axes.Both
};
foreach (var result in tally.Results)
CounterFlow.Add(createCounter(result));
}
protected override void LoadComplete()
{
base.LoadComplete();
FlowDirection.BindValueChanged(direction =>
{
var convertedDirection = getFillDirection(direction.NewValue);
CounterFlow.Direction = convertedDirection;
foreach (var counter in CounterFlow.Children)
counter.Direction.Value = convertedDirection;
}, true);
Mode.BindValueChanged(_ => updateDisplay());
ShowMaxJudgement.BindValueChanged(_ => updateDisplay(), true);
}
private void updateDisplay()
{
for (int i = 0; i < CounterFlow.Children.Count; i++)
{
JudgementCounter counter = CounterFlow.Children[i];
if (shouldShow(i, counter))
counter.Show();
else
counter.Hide();
}
bool shouldShow(int index, JudgementCounter counter)
{
if (index == 0 && !ShowMaxJudgement.Value)
return false;
if (counter.Result.Type.IsBasic())
return true;
switch (Mode.Value)
{
case DisplayMode.Simple:
return false;
case DisplayMode.Normal:
return !counter.Result.Type.IsBonus();
case DisplayMode.All:
return true;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private FillDirection getFillDirection(Direction flow)
{
switch (flow)
{
case Direction.Horizontal:
return FillDirection.Horizontal;
case Direction.Vertical:
return FillDirection.Vertical;
default:
throw new ArgumentOutOfRangeException(nameof(flow), flow, @"Unsupported direction");
}
}
private JudgementCounter createCounter(JudgementTally.JudgementCount info) =>
new JudgementCounter(info)
{
State = { Value = Visibility.Hidden },
ShowName = { BindTarget = ShowJudgementNames }
};
public enum DisplayMode
{
Simple,
Normal,
All
}
}
}

View File

@ -0,0 +1,60 @@
// 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.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play.HUD.JudgementCounter
{
/// <summary>
/// Keeps track of judgements for a current play session, exposing bindable counts which can
/// be used for display purposes.
/// </summary>
public partial class JudgementTally : CompositeDrawable
{
[Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!;
public List<JudgementCount> Results = new List<JudgementCount>();
[BackgroundDependencyLoader]
private void load(IBindable<RulesetInfo> ruleset)
{
foreach (var result in ruleset.Value.CreateInstance().GetHitResults())
{
Results.Add(new JudgementCount
{
Type = result.result,
ResultCount = new BindableInt()
});
}
}
protected override void LoadComplete()
{
base.LoadComplete();
scoreProcessor.NewJudgement += judgement => updateCount(judgement, false);
scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true);
}
private void updateCount(JudgementResult judgement, bool revert)
{
foreach (JudgementCount result in Results.Where(result => result.Type == judgement.Type))
result.ResultCount.Value = revert ? result.ResultCount.Value - 1 : result.ResultCount.Value + 1;
}
public struct JudgementCount
{
public HitResult Type { get; set; }
public BindableInt ResultCount { get; set; }
}
}
}

View File

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

View File

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

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond; using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osu.Game.Screens.Play.HUD.JudgementCounter;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -59,6 +60,9 @@ namespace osu.Game.Screens.Play
[Cached] [Cached]
private readonly ClicksPerSecondCalculator clicksPerSecondCalculator; private readonly ClicksPerSecondCalculator clicksPerSecondCalculator;
[Cached]
private readonly JudgementTally tally;
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true); public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
private readonly DrawableRuleset drawableRuleset; private readonly DrawableRuleset drawableRuleset;
@ -104,6 +108,8 @@ namespace osu.Game.Screens.Play
Children = new Drawable[] Children = new Drawable[]
{ {
CreateFailingLayer(), CreateFailingLayer(),
//Needs to be initialized before skinnable drawables.
tally = new JudgementTally(),
mainComponents = new MainComponentsContainer mainComponents = new MainComponentsContainer
{ {
AlwaysPresent = true, AlwaysPresent = true,

View File

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

View File

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

View File

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

View File

@ -104,6 +104,9 @@ namespace osu.Game.Screens.Select.Leaderboards
protected override APIRequest? FetchScores(CancellationToken cancellationToken) protected override APIRequest? FetchScores(CancellationToken cancellationToken)
{ {
scoreRetrievalRequest?.Cancel();
scoreRetrievalRequest = null;
var fetchBeatmapInfo = BeatmapInfo; var fetchBeatmapInfo = BeatmapInfo;
if (fetchBeatmapInfo == null) if (fetchBeatmapInfo == null)
@ -152,8 +155,6 @@ namespace osu.Game.Screens.Select.Leaderboards
else if (filterMods) else if (filterMods)
requestMods = mods.Value; requestMods = mods.Value;
scoreRetrievalRequest?.Cancel();
var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
newRequest.Success += response => Schedule(() => newRequest.Success += response => Schedule(() =>
{ {

View File

@ -35,6 +35,7 @@ using osu.Game.Collections;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using System.Diagnostics; using System.Diagnostics;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Configuration;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -124,9 +125,20 @@ namespace osu.Game.Screens.Select
[Resolved] [Resolved]
internal IOverlayManager? OverlayManager { get; private set; } internal IOverlayManager? OverlayManager { get; private set; }
private Bindable<bool> configBackgroundBlur { get; set; } = new BindableBool();
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender) private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config)
{ {
configBackgroundBlur = config.GetBindable<bool>(OsuSetting.SongSelectBackgroundBlur);
configBackgroundBlur.BindValueChanged(e =>
{
if (!this.IsCurrentScreen())
return;
ApplyToBackground(b => b.BlurAmount.Value = e.NewValue ? BACKGROUND_BLUR : 0);
});
LoadComponentAsync(Carousel = new BeatmapCarousel LoadComponentAsync(Carousel = new BeatmapCarousel
{ {
AllowSelection = false, // delay any selection until our bindables are ready to make a good choice. AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
@ -742,7 +754,7 @@ namespace osu.Game.Screens.Select
ApplyToBackground(backgroundModeBeatmap => ApplyToBackground(backgroundModeBeatmap =>
{ {
backgroundModeBeatmap.Beatmap = beatmap; backgroundModeBeatmap.Beatmap = beatmap;
backgroundModeBeatmap.BlurAmount.Value = BACKGROUND_BLUR; backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f;
backgroundModeBeatmap.FadeColour(Color4.White, 250); backgroundModeBeatmap.FadeColour(Color4.White, 250);
}); });

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More