1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +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:
name: Build only (iOS)
# change to macos-latest once GitHub finishes migrating all repositories to macOS 12.
runs-on: macos-12
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
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
uses: actions/setup-dotnet@v1
with:

View File

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

View File

@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()),
new CatchModHidden(),
new CatchModFlashlight(),
new ModAccuracyChallenge(),
};
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 ManiaModFadeIn(), new ManiaModHidden()),
new ManiaModFlashlight(),
new ModAccuracyChallenge(),
};
case ModType.Conversion:

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
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();
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 osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI;
using osu.Framework.Bindables;
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 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();
protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;

View File

@ -3,8 +3,10 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Mania.Mods
/// </summary>
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)
{
ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
@ -36,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
c.RelativeSizeAxes = Axes.Both;
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>
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()
{
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)
?? 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) =>
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using osu.Framework.Input;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@ -28,6 +27,7 @@ namespace osu.Game.Rulesets.Osu
/// </remarks>
public bool AllowGameplayInputs
{
get => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs;
set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value;
}
@ -45,6 +45,12 @@ namespace osu.Game.Rulesets.Osu
{
}
[BackgroundDependencyLoader]
private void load()
{
Add(new OsuTouchInputMapper(this) { RelativeSizeAxes = Axes.Both });
}
protected override bool Handle(UIEvent e)
{
if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
@ -52,19 +58,6 @@ namespace osu.Game.Rulesets.Osu
return base.Handle(e);
}
protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
{
if (!AllowUserCursorMovement)
{
// Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse.
// Primarily relied upon by the "autopilot" osu! mod.
var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position);
e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null);
}
return base.HandleMouseTouchStateChange(e);
}
private partial class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
private bool allowGameplayInputs = true;

View File

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

View File

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

View File

@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary>
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>
/// Used to colour the path.
/// </summary>

View File

@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
public override Vector2 PathOffset => snakedPathOffset;
public override Vector2 PathEndOffset => snakedPathEndOffset;
/// <summary>
/// The top-left position of the path when fully snaked.
/// </summary>
@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary>
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!;
[BackgroundDependencyLoader]
@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]);
double lastSnakedStart = SnakedStart ?? 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.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@ -9,30 +8,15 @@ using osu.Game.Rulesets.UI;
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)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
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 TaikoModHidden(),
new TaikoModFlashlight(),
new ModAccuracyChallenge(),
};
case ModType.Conversion:

View File

@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Left;
TimeRange.Value = 7000;
}
[BackgroundDependencyLoader]
@ -60,6 +59,19 @@ namespace osu.Game.Rulesets.Taiko.UI
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()
{
base.UpdateAfterChildren();

View File

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

View File

@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Background
base.LoadComplete();
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.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
@ -42,6 +43,9 @@ namespace osu.Game.Tests.Visual.Editing
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
public override void SetUpSteps()
@ -224,7 +228,8 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName
&& 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));
}
[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]
public void TestCreateMultipleNewDifficultiesSucceeds()
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
@ -19,6 +20,9 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
private ProfileHeader header = null!;
[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));
}
[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]
public void TestOnlineState()
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin);
save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false);
workingBeatmapCache.Invalidate(targetBeatmapSet);
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
@ -280,77 +280,16 @@ namespace osu.Game.Beatmaps
public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
/// <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>
/// <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="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>
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
{
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 virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true);
public void DeleteAllVideos()
{
@ -460,6 +399,74 @@ namespace osu.Game.Beatmaps
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
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.ProfileCoverExpanded, true);
SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full);
SetDefault(OsuSetting.SongSelectBackgroundBlur, true);
// Online settings
SetDefault(OsuSetting.Username, string.Empty);
SetDefault(OsuSetting.Token, string.Empty);
@ -339,6 +343,7 @@ namespace osu.Game.Configuration
ChatDisplayHeight,
BeatmapListingCardSize,
ToolbarClockDisplayMode,
SongSelectBackgroundBlur,
Version,
ShowFirstRunSetup,
ShowConvertedBeatmaps,
@ -375,5 +380,6 @@ namespace osu.Game.Configuration
LastProcessedMetadataId,
SafeAreaConsiderations,
ComboColourNormalisationAmount,
ProfileCoverExpanded,
}
}

View File

@ -31,12 +31,6 @@ namespace osu.Game.Graphics.Backgrounds
/// </summary>
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;
public Color4 ColourLight
@ -115,7 +109,7 @@ namespace osu.Game.Graphics.Backgrounds
private void load(IRenderer renderer, ShaderManager shaders)
{
texture = renderer.WhitePixel;
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder");
}
protected override void LoadComplete()
@ -252,14 +246,17 @@ namespace osu.Game.Graphics.Backgrounds
private class TrianglesDrawNode : DrawNode
{
private float fill = 1f;
protected new Triangles Source => (Triangles)base.Source;
private IShader shader;
private Texture texture;
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;
public TrianglesDrawNode(Triangles source)
@ -290,29 +287,28 @@ namespace osu.Game.Graphics.Backgrounds
}
shader.Bind();
Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy;
shader.GetUniform<float>("thickness").UpdateValue(ref fill);
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(
Vector2Extensions.Transform(particle.Position * size, DrawInfo.Matrix),
Vector2Extensions.Transform(particle.Position * size + offset, DrawInfo.Matrix),
Vector2Extensions.Transform(particle.Position * size + new Vector2(-offset.X, offset.Y), DrawInfo.Matrix)
Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f);
Vector2 topRight = topLeft + new Vector2(relativeSize.X, 0f);
Vector2 bottomLeft = topLeft + new Vector2(0f, relativeSize.Y);
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.ApplyChild(particle.Colour);
renderer.DrawTriangle(
texture,
triangle,
colourInfo,
null,
vertexBatch.AddAction,
Vector2.Divide(localInflationAmount, new Vector2(2 * offset.X, offset.Y)));
renderer.DrawQuad(texture, drawQuad, colourInfo, vertexAction: vertexBatch.AddAction);
}
shader.Unbind();

View File

@ -5,6 +5,7 @@
using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Colour;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
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>
/// Returns a foreground text colour that is supposed to contrast well with
/// the supplied <paramref name="backgroundColour"/>.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -4,6 +4,7 @@
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.Cursor;
using osu.Framework.Graphics.Sprites;
@ -12,6 +13,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -23,6 +25,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
public LocalisableString TooltipText { get; private set; }
private OsuSpriteText levelText = null!;
private Sprite sprite = null!;
[Resolved]
private OsuColour osuColour { get; set; } = null!;
public LevelBadge()
{
@ -34,7 +40,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
InternalChildren = new Drawable[]
{
new Sprite
sprite = new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get("Profile/levelbadge"),
@ -58,9 +64,34 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateLevel(UserStatistics.LevelInfo? levelInfo)
{
string level = levelInfo?.Current.ToString() ?? "0";
levelText.Text = level;
TooltipText = UsersStrings.ShowStatsLevel(level);
int level = levelInfo?.Current ?? 0;
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.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@ -15,11 +14,11 @@ using osuTK;
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 Sample? sampleOpen;
@ -27,12 +26,12 @@ namespace osu.Game.Overlays.Profile.Header.Components
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds();
public ExpandDetailsButton()
public ToggleCoverButton()
{
Action = () =>
{
DetailsVisible.Toggle();
(DetailsVisible.Value ? sampleOpen : sampleClose)?.Play();
CoverExpanded.Toggle();
(CoverExpanded.Value ? sampleOpen : sampleClose)?.Play();
};
}
@ -40,19 +39,21 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void load(OverlayColourProvider colourProvider, AudioManager audio)
{
IdleColour = colourProvider.Background2;
HoverColour = colourProvider.Background2.Lighten(0.2f);
HoverColour = colourProvider.Background1;
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
AutoSizeAxes = Axes.None;
Size = new Vector2(30);
Child = icon = new SpriteIcon
{
Anchor = 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;

View File

@ -7,13 +7,16 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
@ -21,13 +24,15 @@ namespace osu.Game.Overlays.Profile.Header
{
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?>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
private UserCoverBackground cover = null!;
private SupporterIcon supporterTag = null!;
private UpdateableAvatar avatar = null!;
private OsuSpriteText usernameText = null!;
@ -36,11 +41,19 @@ namespace osu.Game.Overlays.Profile.Header
private UpdateableFlag userFlag = null!;
private OsuSpriteText userCountryText = null!;
private GroupBadgeFlow groupBadgeFlow = null!;
private ToggleCoverButton coverToggle = null!;
private Bindable<bool> coverExpanded = null!;
private FillFlowContainer flow = null!;
[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[]
{
@ -51,124 +64,147 @@ namespace osu.Game.Overlays.Profile.Header
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN },
Height = avatar_size,
AutoSizeAxes = Axes.X,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false)
cover = new ProfileCoverBackground
{
Size = new Vector2(avatar_size),
Masking = true,
CornerRadius = avatar_size * 0.25f,
RelativeSizeAxes = Axes.X,
},
new OsuContextMenuContainer
new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Child = new Container
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Padding = new MarginPadding { Left = 10 },
Children = new Drawable[]
flow = new FillFlowContainer
{
new FillFlowContainer
Direction = FillDirection.Horizontal,
Padding = new MarginPadding
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
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)
},
}
Left = UserProfileOverlay.CONTENT_X_MARGIN,
Vertical = vertical_padding
},
new FillFlowContainer
Height = content_height + 2 * vertical_padding,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false)
{
supporterTag = new SupporterIcon
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Height = 20,
Margin = new MarginPadding { Top = 5 }
},
new Box
{
RelativeSizeAxes = Axes.X,
Height = 1.5f,
Margin = new MarginPadding { Top = 10 },
Colour = colourProvider.Light1,
},
new FillFlowContainer
Type = EdgeEffectType.Shadow,
Offset = new Vector2(0, 1),
Radius = 3,
Colour = Colour4.Black.Opacity(0.25f),
}
},
new OsuContextMenuContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 5 },
Direction = FillDirection.Horizontal,
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
userFlag = new UpdateableFlag
new FillFlowContainer
{
Size = new Vector2(28, 20),
ShowPlaceholderOnUnknown = false,
AutoSizeAxes = Axes.Both,
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),
Margin = new MarginPadding { Left = 10 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Colour = colourProvider.Light1,
}
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
Margin = new MarginPadding { Bottom = 5 }
},
new FillFlowContainer
{
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)
{
var user = data?.User;
cover.User = user;
avatar.User = user;
usernameText.Text = user?.Username ?? string.Empty;
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");
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 osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Overlays.Profile.Header;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile
{
public partial class ProfileHeader : TabControlOverlayHeader<LocalisableString>
{
private UserCoverBackground coverContainer = null!;
public Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
private CentreHeaderContainer centreHeaderContainer;
@ -29,8 +23,6 @@ namespace osu.Game.Overlays.Profile
{
ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN;
User.ValueChanged += e => updateDisplay(e.NewValue);
TabControl.AddItem(LayoutStrings.HeaderUsersShow);
// todo: pending implementation.
@ -41,25 +33,7 @@ namespace osu.Game.Overlays.Profile
Debug.Assert(detailHeaderContainer != null);
}
protected override Drawable CreateBackground() =>
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 CreateBackground() => Empty();
protected override Drawable CreateContent() => new FillFlowContainer
{
@ -103,8 +77,6 @@ namespace osu.Game.Overlays.Profile
User = { BindTarget = User }
};
private void updateDisplay(UserProfileData? user) => coverContainer.User = user?.User;
private partial class ProfileHeaderTitle : OverlayTitle
{
public ProfileHeaderTitle()
@ -113,10 +85,5 @@ namespace osu.Game.Overlays.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
{
LabelText = SkinSettingsStrings.GameplayCursorDuringTouch,
Keywords = new[] { @"touchscreen" },
Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch)
},
};

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
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)
{

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,
Current = config.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle),
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Settings
public LocalisableString TooltipText { get; set; }
public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>();
public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
@ -30,9 +32,13 @@ namespace osu.Game.Overlays.Settings
get
{
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,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
},
new BuildDisplay(game.Version, DebugUtils.IsDebugBuild)
new BuildDisplay(game.Version)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@ -81,15 +81,13 @@ namespace osu.Game.Overlays.Settings
private partial class BuildDisplay : OsuAnimatedButton
{
private readonly string version;
private readonly bool isDebug;
[Resolved]
private OsuColour colours { get; set; }
public BuildDisplay(string version, bool isDebug)
public BuildDisplay(string version)
{
this.version = version;
this.isDebug = isDebug;
Content.RelativeSizeAxes = Axes.Y;
Content.AutoSizeAxes = AutoSizeAxes = Axes.X;
@ -99,8 +97,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader(true)]
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
{
@ -110,7 +107,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
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
using System;
using System.Linq;
using osu.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -125,10 +126,21 @@ namespace osu.Game.Rulesets.Edit
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
/// <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>
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>
/// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections.
/// </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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Humanizer;
using osu.Framework.Bindables;
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 Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray();
private int retries;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1;
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()
{

View File

@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
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 comboPortion;
@ -145,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring
public ScoreProcessor(Ruleset ruleset)
{
this.ruleset = ruleset;
Ruleset = ruleset;
accuracyPortion = DefaultAccuracyPortion;
comboPortion = DefaultComboPortion;
@ -291,8 +295,8 @@ namespace osu.Game.Rulesets.Scoring
[Pure]
public double ComputeAccuracy(ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
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.
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
@ -312,8 +316,8 @@ namespace osu.Game.Rulesets.Scoring
[Pure]
public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
extractScoringValues(scoreInfo, out var current, out var maximum);
@ -552,7 +556,7 @@ namespace osu.Game.Rulesets.Scoring
break;
default:
maxResult = maxBasicResult ??= ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
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
private Vector2[] movementBlueprintOriginalPositions;
private Vector2[][] movementBlueprintsOriginalPositions;
private SelectionBlueprint<T>[] movementBlueprints;
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
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray();
movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray();
return true;
}
@ -480,26 +480,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null)
return false;
Debug.Assert(movementBlueprintOriginalPositions != null);
Debug.Assert(movementBlueprintsOriginalPositions != null);
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
if (snapProvider != null)
{
// check for positional snap for every object in selection (for things like object-object snapping)
for (int i = 0; i < movementBlueprintOriginalPositions.Length; i++)
for (int i = 0; i < movementBlueprints.Length; i++)
{
Vector2 originalPosition = movementBlueprintOriginalPositions[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)))
if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i]))
return true;
}
}
@ -508,7 +497,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled;
// Retrieve a snapped position.
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
@ -521,6 +510,36 @@ namespace osu.Game.Screens.Edit.Compose.Components
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) =>
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)
return false;
movementBlueprintOriginalPositions = null;
movementBlueprintsOriginalPositions = null;
movementBlueprints = null;
return true;

View File

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

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]
private IGameplayClock gameplayClock { get; set; } = null!;
[Resolved(canBeNull: true)]
private DrawableRuleset? drawableRuleset { get; set; }
[Resolved]
private IFrameStableClock? frameStableClock { get; set; }
public int Value { get; private set; }
// Even though `FrameStabilityContainer` caches as a `GameplayClock`, we need to check it directly via `drawableRuleset`
// as this calculator is not contained within the `FrameStabilityContainer` and won't see the dependency.
private IGameplayClock clock => drawableRuleset?.FrameStableClock ?? gameplayClock;
private IGameplayClock clock => frameStableClock ?? gameplayClock;
public ClicksPerSecondCalculator()
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osu.Game.Screens.Play.HUD.JudgementCounter;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Localisation;
@ -59,6 +60,9 @@ namespace osu.Game.Screens.Play
[Cached]
private readonly ClicksPerSecondCalculator clicksPerSecondCalculator;
[Cached]
private readonly JudgementTally tally;
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
private readonly DrawableRuleset drawableRuleset;
@ -104,6 +108,8 @@ namespace osu.Game.Screens.Play
Children = new Drawable[]
{
CreateFailingLayer(),
//Needs to be initialized before skinnable drawables.
tally = new JudgementTally(),
mainComponents = new MainComponentsContainer
{
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.
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
@ -1070,7 +1072,7 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(ScreenExitEvent e)
{
screenSuspension?.RemoveAndDisposeImmediately();
failAnimationLayer?.RemoveFilters();
failAnimationLayer?.Stop();
if (LoadedBeatmapSuccessfully)
{

View File

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

View File

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

View File

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

View File

@ -35,6 +35,7 @@ using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using System.Diagnostics;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Configuration;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
@ -124,9 +125,20 @@ namespace osu.Game.Screens.Select
[Resolved]
internal IOverlayManager? OverlayManager { get; private set; }
private Bindable<bool> configBackgroundBlur { get; set; } = new BindableBool();
[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
{
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 =>
{
backgroundModeBeatmap.Beatmap = beatmap;
backgroundModeBeatmap.BlurAmount.Value = BACKGROUND_BLUR;
backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f;
backgroundModeBeatmap.FadeColour(Color4.White, 250);
});

View File

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

View File

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

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