diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5c11f91994..e60e0a39ae 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,17 +13,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v3
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@@ -77,10 +77,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Install .NET 6.0.x
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@@ -94,7 +94,7 @@ jobs:
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
- name: Upload Test Results
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
@@ -106,10 +106,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Install .NET 6.0.x
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@@ -125,10 +125,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Install .NET 6.0.x
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml
index 9e11ab6663..2c6ec17e18 100644
--- a/.github/workflows/diffcalc.yml
+++ b/.github/workflows/diffcalc.yml
@@ -48,8 +48,8 @@ jobs:
CONTINUE="no"
fi
- echo "::set-output name=continue::${CONTINUE}"
- echo "::set-output name=matrix::${MATRIX_JSON}"
+ echo "continue=${CONTINUE}" >> $GITHUB_OUTPUT
+ echo "matrix=${MATRIX_JSON}" >> $GITHUB_OUTPUT
diffcalc:
name: Run
runs-on: self-hosted
@@ -80,34 +80,34 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- echo "::set-output name=branchname::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')"
- echo "::set-output name=repo::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')"
+ echo "branchname=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')" >> $GITHUB_OUTPUT
+ echo "repo=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')" >> $GITHUB_OUTPUT
# Checkout osu
- name: Checkout osu (master)
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
path: 'master/osu'
- name: Checkout osu (pr)
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
path: 'pr/osu'
repository: ${{ steps.upstreambranch.outputs.repo }}
ref: ${{ steps.upstreambranch.outputs.branchname }}
- name: Checkout osu-difficulty-calculator (master)
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: ppy/osu-difficulty-calculator
path: 'master/osu-difficulty-calculator'
- name: Checkout osu-difficulty-calculator (pr)
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
repository: ppy/osu-difficulty-calculator
path: 'pr/osu-difficulty-calculator'
- name: Install .NET 5.0.x
- uses: actions/setup-dotnet@v1
+ uses: actions/setup-dotnet@v3
with:
dotnet-version: "5.0.x"
diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml
index cce3f23e5f..ff4165c414 100644
--- a/.github/workflows/sentry-release.yml
+++ b/.github/workflows/sentry-release.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
with:
fetch-depth: 0
diff --git a/README.md b/README.md
index f3f025fa10..eb2fe6d0eb 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,7 @@ When it comes to contributing to the project, the two main things you can do to
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
-For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
+We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence
diff --git a/osu.Android.props b/osu.Android.props
index 4e580a6919..4b89e82729 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -11,7 +11,7 @@
manifestmerger.jar
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index b3f6370ccb..d92fea27bf 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
@@ -139,7 +138,17 @@ namespace osu.Desktop
desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.Title = Name;
- desktopWindow.DragDrop += f => fileDrop(new[] { f });
+ desktopWindow.DragDrop += f =>
+ {
+ // on macOS, URL associations are handled via SDL_DROPFILE events.
+ if (f.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
+ {
+ HandleLink(f);
+ return;
+ }
+
+ fileDrop(new[] { f });
+ };
}
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
@@ -151,10 +160,6 @@ namespace osu.Desktop
{
lock (importableFiles)
{
- string firstExtension = Path.GetExtension(filePaths.First());
-
- if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return;
-
importableFiles.AddRange(filePaths);
Logger.Log($"Adding {filePaths.Length} files for import");
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs b/osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs
deleted file mode 100644
index 64ff3f7151..0000000000
--- a/osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using Foundation;
-using osu.Framework.iOS;
-using osu.Game.Tests;
-
-namespace osu.Game.Rulesets.Catch.Tests.iOS
-{
- [Register("AppDelegate")]
- public class AppDelegate : GameAppDelegate
- {
- protected override Framework.Game CreateGame() => new OsuTestBrowser();
- }
-}
diff --git a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
index 1fcb0aa427..d097c6a698 100644
--- a/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Catch.Tests.iOS/Application.cs
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using UIKit;
+using osu.Framework.iOS;
+using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
@@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, null, typeof(AppDelegate));
+ GameApplication.Main(new OsuTestBrowser());
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index e59a0a0431..6efb415880 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable CircleSize { get; } = new DifficultyBindable
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.CircleSize,
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 1c52c092ec..ab754e51f7 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.TopCentre;
Size = new Vector2(BASE_SIZE);
+
if (difficulty != null)
Scale = calculateScale(difficulty);
@@ -333,8 +334,11 @@ namespace osu.Game.Rulesets.Catch.UI
base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
+
body.Scale = scaleFromDirection;
- caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
+ // Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit.
+ caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One);
+ hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs b/osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs
deleted file mode 100644
index a528634f3b..0000000000
--- a/osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using Foundation;
-using osu.Framework.iOS;
-using osu.Game.Tests;
-
-namespace osu.Game.Rulesets.Mania.Tests.iOS
-{
- [Register("AppDelegate")]
- public class AppDelegate : GameAppDelegate
- {
- protected override Framework.Game CreateGame() => new OsuTestBrowser();
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
index a508198f7f..75a5a73058 100644
--- a/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Mania.Tests.iOS/Application.cs
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using UIKit;
+using osu.Framework.iOS;
+using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
@@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, null, typeof(AppDelegate));
+ GameApplication.Main(new OsuTestBrowser());
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
index 98046320cb..4e50fd924c 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaPlayer.cs
@@ -3,6 +3,9 @@
#nullable disable
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Game.Rulesets.Mania.Configuration;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
@@ -10,5 +13,19 @@ namespace osu.Game.Rulesets.Mania.Tests
public partial class TestSceneManiaPlayer : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("change direction to down", () => changeDirectionTo(ManiaScrollingDirection.Down));
+ AddStep("change direction to up", () => changeDirectionTo(ManiaScrollingDirection.Up));
+ }
+
+ private void changeDirectionTo(ManiaScrollingDirection direction)
+ {
+ var rulesetConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(new ManiaRuleset()).AsNonNull();
+ rulesetConfig.SetValue(ManiaRulesetSetting.ScrollDirection, direction);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 25d0573a82..6e1c6cf80f 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -236,6 +236,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
};
// Position and resize the body to lie half-way under the head and the tail notes.
+ // The rationale for this is account for heads/tails with corner radius.
bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2;
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index 20ea962994..e7326df07d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
- protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
+ protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
public DrawableHoldNoteTail()
: this(null)
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
index e32de6f3f3..d490d3f944 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
@@ -43,9 +43,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
largeFaint = new Container
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
+ Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
Masking = true,
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Blending = BlendingParameters.Additive,
@@ -80,11 +79,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
if (direction.NewValue == ScrollingDirection.Up)
{
Anchor = Anchor.TopCentre;
+ largeFaint.Anchor = Anchor.TopCentre;
+ largeFaint.Origin = Anchor.TopCentre;
Y = ArgonNotePiece.NOTE_HEIGHT / 2;
}
else
{
Anchor = Anchor.BottomCentre;
+ largeFaint.Anchor = Anchor.BottomCentre;
+ largeFaint.Origin = Anchor.BottomCentre;
Y = -ArgonNotePiece.NOTE_HEIGHT / 2;
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
index 4ffb4a435b..cf5931231c 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private void load(IScrollingInfo scrollingInfo)
{
RelativeSizeAxes = Axes.X;
- Height = ArgonNotePiece.NOTE_HEIGHT;
+ Height = ArgonNotePiece.NOTE_HEIGHT * ArgonNotePiece.NOTE_ACCENT_RATIO;
Masking = true;
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
index 1f52f5f15f..57fa1c10ae 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
@@ -20,10 +20,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
public partial class ArgonHoldBodyPiece : CompositeDrawable, IHoldNoteBody
{
protected readonly Bindable AccentColour = new Bindable();
- protected readonly IBindable IsHitting = new Bindable();
private Drawable background = null!;
- private Box foreground = null!;
+ private ArgonHoldNoteHittingLayer hittingLayer = null!;
public ArgonHoldBodyPiece()
{
@@ -32,7 +31,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
// Without this, the width of the body will be slightly larger than the head/tail.
Masking = true;
CornerRadius = ArgonNotePiece.CORNER_RADIUS;
- Blending = BlendingParameters.Additive;
}
[BackgroundDependencyLoader(true)]
@@ -41,12 +39,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
InternalChildren = new[]
{
background = new Box { RelativeSizeAxes = Axes.Both },
- foreground = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Blending = BlendingParameters.Additive,
- Alpha = 0,
- },
+ hittingLayer = new ArgonHoldNoteHittingLayer()
};
if (drawableObject != null)
@@ -54,44 +47,19 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
var holdNote = (DrawableHoldNote)drawableObject;
AccentColour.BindTo(holdNote.AccentColour);
- IsHitting.BindTo(holdNote.IsHitting);
+ hittingLayer.AccentColour.BindTo(holdNote.AccentColour);
+ ((IBindable)hittingLayer.IsHitting).BindTo(holdNote.IsHitting);
}
AccentColour.BindValueChanged(colour =>
{
- background.Colour = colour.NewValue.Darken(1.2f);
- foreground.Colour = colour.NewValue.Opacity(0.2f);
+ background.Colour = colour.NewValue.Darken(0.6f);
}, true);
-
- IsHitting.BindValueChanged(hitting =>
- {
- const float animation_length = 50;
-
- foreground.ClearTransforms();
-
- if (hitting.NewValue)
- {
- // wait for the next sync point
- double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
-
- using (foreground.BeginDelayedSequence(synchronisedOffset))
- {
- foreground.FadeTo(1, animation_length).Then()
- .FadeTo(0.5f, animation_length)
- .Loop();
- }
- }
- else
- {
- foreground.FadeOut(animation_length);
- }
- });
}
public void Recycle()
{
- foreground.ClearTransforms();
- foreground.Alpha = 0;
+ hittingLayer.Recycle();
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteHeadPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteHeadPiece.cs
new file mode 100644
index 0000000000..b9cc73c75c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteHeadPiece.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ internal partial class ArgonHoldNoteHeadPiece : ArgonNotePiece
+ {
+ protected override Drawable CreateIcon() => new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 2,
+ Size = new Vector2(20, 5),
+ };
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteHittingLayer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteHittingLayer.cs
new file mode 100644
index 0000000000..9e7afa8b9e
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteHittingLayer.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osuTK.Graphics;
+using Box = osu.Framework.Graphics.Shapes.Box;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public partial class ArgonHoldNoteHittingLayer : Box
+ {
+ public readonly Bindable AccentColour = new Bindable();
+ public readonly Bindable IsHitting = new Bindable();
+
+ public ArgonHoldNoteHittingLayer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ Blending = BlendingParameters.Additive;
+ Alpha = 0;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AccentColour.BindValueChanged(colour =>
+ {
+ Colour = colour.NewValue.Lighten(0.2f).Opacity(0.3f);
+ }, true);
+
+ IsHitting.BindValueChanged(hitting =>
+ {
+ const float animation_length = 80;
+
+ ClearTransforms();
+
+ if (hitting.NewValue)
+ {
+ // wait for the next sync point
+ double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
+
+ using (BeginDelayedSequence(synchronisedOffset))
+ {
+ this.FadeTo(1, animation_length, Easing.OutSine).Then()
+ .FadeTo(0.5f, animation_length, Easing.InSine)
+ .Loop();
+ }
+ }
+ else
+ {
+ this.FadeOut(animation_length);
+ }
+ }, true);
+ }
+
+ public void Recycle()
+ {
+ ClearTransforms();
+ Alpha = 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
index 428439d52c..efd7f4f280 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
@@ -5,8 +5,10 @@ using osu.Framework.Allocation;
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.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -16,47 +18,68 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
internal partial class ArgonHoldNoteTailPiece : CompositeDrawable
{
+ [Resolved]
+ private DrawableHitObject? drawableObject { get; set; }
+
private readonly IBindable direction = new Bindable();
private readonly IBindable accentColour = new Bindable();
- private readonly Box shadeBackground;
- private readonly Box shadeForeground;
+ private readonly Box foreground;
+ private readonly ArgonHoldNoteHittingLayer hittingLayer;
+ private readonly Box foregroundAdditive;
public ArgonHoldNoteTailPiece()
{
RelativeSizeAxes = Axes.X;
Height = ArgonNotePiece.NOTE_HEIGHT;
- CornerRadius = ArgonNotePiece.CORNER_RADIUS;
- Masking = true;
-
InternalChildren = new Drawable[]
{
- shadeBackground = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
new Container
{
- RelativeSizeAxes = Axes.Both,
- Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ Height = ArgonNotePiece.NOTE_HEIGHT,
CornerRadius = ArgonNotePiece.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
- shadeForeground = new Box
+ new Box
{
RelativeSizeAxes = Axes.Both,
+ Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Colour4.Black),
+ // Avoid ugly single pixel overlap.
+ Height = 0.9f,
},
- },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ foreground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ hittingLayer = new ArgonHoldNoteHittingLayer(),
+ foregroundAdditive = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Height = 0.5f,
+ },
+ },
+ },
+ }
},
};
}
[BackgroundDependencyLoader(true)]
- private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
+ private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
@@ -65,9 +88,24 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
accentColour.BindTo(drawableObject.AccentColour);
accentColour.BindValueChanged(onAccentChanged, true);
+
+ drawableObject.HitObjectApplied += hitObjectApplied;
}
}
+ private void hitObjectApplied(DrawableHitObject drawableHitObject)
+ {
+ var holdNoteTail = (DrawableHoldNoteTail)drawableHitObject;
+
+ hittingLayer.Recycle();
+
+ hittingLayer.AccentColour.UnbindBindings();
+ hittingLayer.AccentColour.BindTo(holdNoteTail.HoldNote.AccentColour);
+
+ hittingLayer.IsHitting.UnbindBindings();
+ ((IBindable)hittingLayer.IsHitting).BindTo(holdNoteTail.HoldNote.IsHitting);
+ }
+
private void onDirectionChanged(ValueChangedEvent direction)
{
Scale = new Vector2(1, direction.NewValue == ScrollingDirection.Up ? -1 : 1);
@@ -75,8 +113,20 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private void onAccentChanged(ValueChangedEvent accent)
{
- shadeBackground.Colour = accent.NewValue.Darken(1.7f);
- shadeForeground.Colour = accent.NewValue.Darken(1.1f);
+ foreground.Colour = accent.NewValue.Darken(0.6f); // matches body
+
+ foregroundAdditive.Colour = ColourInfo.GradientVertical(
+ accent.NewValue.Opacity(0.4f),
+ accent.NewValue.Opacity(0)
+ );
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableObject != null)
+ drawableObject.HitObjectApplied -= hitObjectApplied;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
index 2a5bce255c..3a519283f1 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
@@ -26,7 +26,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
private readonly IBindable accentColour = new Bindable();
private readonly Box colouredBox;
- private readonly Box shadow;
public ArgonNotePiece()
{
@@ -36,11 +35,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
CornerRadius = CORNER_RADIUS;
Masking = true;
- InternalChildren = new Drawable[]
+ InternalChildren = new[]
{
- shadow = new Box
+ new Box
{
RelativeSizeAxes = Axes.Both,
+ Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Colour4.Black)
},
new Container
{
@@ -65,18 +65,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
RelativeSizeAxes = Axes.X,
Height = CORNER_RADIUS * 2,
},
- new SpriteIcon
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Y = 4,
- Icon = FontAwesome.Solid.AngleDown,
- Size = new Vector2(20),
- Scale = new Vector2(1, 0.7f)
- }
+ CreateIcon(),
};
}
+ protected virtual Drawable CreateIcon() => new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 4,
+ // TODO: replace with a non-squashed version.
+ // The 0.7f height scale should be removed.
+ Icon = FontAwesome.Solid.AngleDown,
+ Size = new Vector2(20),
+ Scale = new Vector2(1, 0.7f)
+ };
+
[BackgroundDependencyLoader(true)]
private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
{
@@ -105,8 +109,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
accent.NewValue.Lighten(0.1f),
accent.NewValue
);
-
- shadow.Colour = accent.NewValue.Darken(0.5f);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
index 057b7eb0d9..007d02400a 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
@@ -50,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return new ArgonHoldNoteTailPiece();
case ManiaSkinComponents.HoldNoteHead:
+ return new ArgonHoldNoteHeadPiece();
+
case ManiaSkinComponents.Note:
return new ArgonNotePiece();
@@ -69,12 +71,23 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return base.GetDrawableComponent(lookup);
}
+ private static readonly Color4 colour_special_column = new Color4(169, 106, 255, 255);
+
+ private const int total_colours = 6;
+
+ private static readonly Color4 colour_yellow = new Color4(255, 197, 40, 255);
+ private static readonly Color4 colour_orange = new Color4(252, 109, 1, 255);
+ private static readonly Color4 colour_pink = new Color4(213, 35, 90, 255);
+ private static readonly Color4 colour_purple = new Color4(203, 60, 236, 255);
+ private static readonly Color4 colour_cyan = new Color4(72, 198, 255, 255);
+ private static readonly Color4 colour_green = new Color4(100, 192, 92, 255);
+
public override IBindable? GetConfig(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
{
- int column = maniaLookup.ColumnIndex ?? 0;
- var stage = beatmap.GetStageForColumnIndex(column);
+ int columnIndex = maniaLookup.ColumnIndex ?? 0;
+ var stage = beatmap.GetStageForColumnIndex(columnIndex);
switch (maniaLookup.Lookup)
{
@@ -87,53 +100,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
return SkinUtils.As(new Bindable(
- stage.IsSpecialColumn(column) ? 120 : 60
+ stage.IsSpecialColumn(columnIndex) ? 120 : 60
));
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
- Color4 colour;
-
- const int total_colours = 7;
-
- if (stage.IsSpecialColumn(column))
- colour = new Color4(159, 101, 255, 255);
- else
- {
- switch (column % total_colours)
- {
- case 0:
- colour = new Color4(240, 216, 0, 255);
- break;
-
- case 1:
- colour = new Color4(240, 101, 0, 255);
- break;
-
- case 2:
- colour = new Color4(240, 0, 130, 255);
- break;
-
- case 3:
- colour = new Color4(192, 0, 240, 255);
- break;
-
- case 4:
- colour = new Color4(0, 96, 240, 255);
- break;
-
- case 5:
- colour = new Color4(0, 226, 240, 255);
- break;
-
- case 6:
- colour = new Color4(0, 240, 96, 255);
- break;
-
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
+ var colour = getColourForLayout(columnIndex, stage);
return SkinUtils.As(new Bindable(colour));
}
@@ -141,5 +113,203 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return base.GetConfig(lookup);
}
+
+ private Color4 getColourForLayout(int columnIndex, StageDefinition stage)
+ {
+ // Account for cases like dual-stage (assume that all stages have the same column count for now).
+ columnIndex %= stage.Columns;
+
+ // For now, these are defined per column count as per https://user-images.githubusercontent.com/50823728/218038463-b450f46c-ef21-4551-b133-f866be59970c.png
+ // See https://github.com/ppy/osu/discussions/21996 for discussion.
+ switch (stage.Columns)
+ {
+ case 1:
+ return colour_yellow;
+
+ case 2:
+ switch (columnIndex)
+ {
+ case 0: return colour_green;
+
+ case 1: return colour_cyan;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+
+ case 3:
+ switch (columnIndex)
+ {
+ case 0: return colour_pink;
+
+ case 1: return colour_orange;
+
+ case 2: return colour_yellow;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+
+ case 4:
+ switch (columnIndex)
+ {
+ case 0: return colour_yellow;
+
+ case 1: return colour_orange;
+
+ case 2: return colour_pink;
+
+ case 3: return colour_purple;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+
+ case 5:
+ switch (columnIndex)
+ {
+ case 0: return colour_pink;
+
+ case 1: return colour_orange;
+
+ case 2: return colour_yellow;
+
+ case 3: return colour_green;
+
+ case 4: return colour_cyan;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+
+ case 6:
+ switch (columnIndex)
+ {
+ case 0: return colour_pink;
+
+ case 1: return colour_orange;
+
+ case 2: return colour_yellow;
+
+ case 3: return colour_cyan;
+
+ case 4: return colour_purple;
+
+ case 5: return colour_pink;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+
+ case 7:
+ switch (columnIndex)
+ {
+ case 0: return colour_pink;
+
+ case 1: return colour_cyan;
+
+ case 2: return colour_pink;
+
+ case 3: return colour_special_column;
+
+ case 4: return colour_green;
+
+ case 5: return colour_cyan;
+
+ case 6: return colour_green;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+
+ case 8:
+ switch (columnIndex)
+ {
+ case 0: return colour_purple;
+
+ case 1: return colour_pink;
+
+ case 2: return colour_orange;
+
+ case 3: return colour_yellow;
+
+ case 4: return colour_yellow;
+
+ case 5: return colour_orange;
+
+ case 6: return colour_pink;
+
+ case 7: return colour_purple;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+
+ case 9:
+ switch (columnIndex)
+ {
+ case 0: return colour_purple;
+
+ case 1: return colour_pink;
+
+ case 2: return colour_orange;
+
+ case 3: return colour_yellow;
+
+ case 4: return colour_special_column;
+
+ case 5: return colour_yellow;
+
+ case 6: return colour_orange;
+
+ case 7: return colour_pink;
+
+ case 8: return colour_purple;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+
+ case 10:
+ switch (columnIndex)
+ {
+ case 0: return colour_purple;
+
+ case 1: return colour_pink;
+
+ case 2: return colour_orange;
+
+ case 3: return colour_yellow;
+
+ case 4: return colour_cyan;
+
+ case 5: return colour_green;
+
+ case 6: return colour_yellow;
+
+ case 7: return colour_orange;
+
+ case 8: return colour_pink;
+
+ case 9: return colour_purple;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ // fallback for unhandled scenarios
+
+ if (stage.IsSpecialColumn(columnIndex))
+ return colour_special_column;
+
+ switch (columnIndex % total_colours)
+ {
+ case 0: return colour_yellow;
+
+ case 1: return colour_orange;
+
+ case 2: return colour_pink;
+
+ case 3: return colour_purple;
+
+ case 4: return colour_cyan;
+
+ case 5: return colour_green;
+
+ default: throw new ArgumentOutOfRangeException();
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs b/osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs
deleted file mode 100644
index fa40a8536e..0000000000
--- a/osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using Foundation;
-using osu.Framework.iOS;
-using osu.Game.Tests;
-
-namespace osu.Game.Rulesets.Osu.Tests.iOS
-{
- [Register("AppDelegate")]
- public class AppDelegate : GameAppDelegate
- {
- protected override Framework.Game CreateGame() => new OsuTestBrowser();
- }
-}
diff --git a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
index 6ef29fa68e..f9059014a5 100644
--- a/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Osu.Tests.iOS/Application.cs
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using UIKit;
+using osu.Framework.iOS;
+using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
@@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, null, typeof(AppDelegate));
+ GameApplication.Main(new OsuTestBrowser());
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs
index 72bcec6045..bb424eb587 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs
@@ -21,7 +21,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI.Cursor;
-using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
@@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private OsuConfigManager config { get; set; } = null!;
- private TestActionKeyCounter leftKeyCounter = null!;
+ private DefaultKeyCounter leftKeyCounter = null!;
- private TestActionKeyCounter rightKeyCounter = null!;
+ private DefaultKeyCounter rightKeyCounter = null!;
private OsuInputManager osuInputManager = null!;
@@ -59,14 +59,14 @@ namespace osu.Game.Rulesets.Osu.Tests
Origin = Anchor.Centre,
Children = new Drawable[]
{
- leftKeyCounter = new TestActionKeyCounter(OsuAction.LeftButton)
+ leftKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.LeftButton))
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Depth = float.MinValue,
X = -100,
},
- rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton)
+ rightKeyCounter = new DefaultKeyCounter(new TestActionKeyCounterTrigger(OsuAction.RightButton))
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
@@ -150,6 +150,42 @@ namespace osu.Game.Rulesets.Osu.Tests
assertKeyCounter(1, 1);
}
+ [Test]
+ public void TestPositionalTrackingAfterLongDistanceTravelled()
+ {
+ // When a single touch has already travelled enough distance on screen, it should remain as the positional
+ // tracking touch until released (unless a direct touch occurs).
+
+ beginTouch(TouchSource.Touch1);
+
+ assertKeyCounter(1, 0);
+ checkPressed(OsuAction.LeftButton);
+ checkPosition(TouchSource.Touch1);
+
+ // cover some distance
+ beginTouch(TouchSource.Touch1, new Vector2(0));
+ beginTouch(TouchSource.Touch1, new Vector2(9999));
+ beginTouch(TouchSource.Touch1, new Vector2(0));
+ beginTouch(TouchSource.Touch1, new Vector2(9999));
+ beginTouch(TouchSource.Touch1);
+
+ beginTouch(TouchSource.Touch2);
+
+ assertKeyCounter(1, 1);
+ checkNotPressed(OsuAction.LeftButton);
+ checkPressed(OsuAction.RightButton);
+ // in this case, touch 2 should not become the positional tracking touch.
+ checkPosition(TouchSource.Touch1);
+
+ // even if the second touch moves on the screen, the original tracking touch is retained.
+ beginTouch(TouchSource.Touch2, new Vector2(0));
+ beginTouch(TouchSource.Touch2, new Vector2(9999));
+ beginTouch(TouchSource.Touch2, new Vector2(0));
+ beginTouch(TouchSource.Touch2, new Vector2(9999));
+
+ checkPosition(TouchSource.Touch1);
+ }
+
[Test]
public void TestPositionalInputUpdatesOnlyFromMostRecentTouch()
{
@@ -562,8 +598,8 @@ namespace osu.Game.Rulesets.Osu.Tests
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));
+ AddAssert($"The left key was pressed {left} times", () => leftKeyCounter.CountPresses.Value, () => Is.EqualTo(left));
+ AddAssert($"The right key was pressed {right} times", () => rightKeyCounter.CountPresses.Value, () => Is.EqualTo(right));
}
private void releaseAllTouches()
@@ -579,11 +615,11 @@ namespace osu.Game.Rulesets.Osu.Tests
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
+ public partial class TestActionKeyCounterTrigger : InputTrigger, IKeyBindingHandler
{
public OsuAction Action { get; }
- public TestActionKeyCounter(OsuAction action)
+ public TestActionKeyCounterTrigger(OsuAction action)
: base(action.ToString())
{
Action = action;
@@ -593,8 +629,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
if (e.Action == Action)
{
- IsLit = true;
- Increment();
+ Activate();
}
return false;
@@ -602,7 +637,8 @@ namespace osu.Game.Rulesets.Osu.Tests
public void OnReleased(KeyBindingReleaseEvent e)
{
- if (e.Action == Action) IsLit = false;
+ if (e.Action == Action)
+ Deactivate();
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
index 3e161089cd..d6409279a4 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive
- || (ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
+ || (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
protected OsuSelectionBlueprint(T hitObject)
: base(hitObject)
diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
index a824f202ec..9d64c354e2 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
@@ -252,13 +252,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
renderer.SetBlend(BlendingParameters.Additive);
renderer.PushLocalMatrix(DrawInfo.Matrix);
- TextureShader.Bind();
+ BindTextureShader(renderer);
+
texture.Bind();
for (int i = 0; i < points.Count; i++)
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
- TextureShader.Unbind();
+ UnbindTextureShader(renderer);
renderer.PopLocalMatrix();
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs
index 8df1c35b5c..5277a1f7d6 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs
@@ -22,6 +22,13 @@ namespace osu.Game.Rulesets.Osu.UI
///
private readonly List trackedTouches = new List();
+ ///
+ /// The distance (in local pixels) that a touch must move before being considered a permanent tracking touch.
+ /// After this distance is covered, any extra touches on the screen will be considered as button inputs, unless
+ /// a new touch directly interacts with a hit circle.
+ ///
+ private const float distance_before_position_tracking_lock_in = 100;
+
private TrackedTouch? positionTrackingTouch;
private readonly OsuInputManager osuInputManager;
@@ -97,26 +104,32 @@ namespace osu.Game.Rulesets.Osu.UI
return;
}
- // ..or if the current position tracking touch was not a direct touch (this one is debatable and may be change in the future, but it's the simplest way to handle)
- if (!positionTrackingTouch.DirectTouch)
+ // ..or if the current position tracking touch was not a direct touch (and didn't travel across the screen too far).
+ if (!positionTrackingTouch.DirectTouch && positionTrackingTouch.DistanceTravelled < distance_before_position_tracking_lock_in)
{
positionTrackingTouch = newTouch;
return;
}
// In the case the new touch was not used for position tracking, we should also check the previous position tracking touch.
- // If it was a direct touch and still has its action pressed, that action should be released.
+ // If it still has its action pressed, that action should be released.
//
// This is done to allow tracking with the initial touch while still having both Left/Right actions available for alternating with two more touches.
- if (positionTrackingTouch.DirectTouch && positionTrackingTouch.Action is OsuAction directTouchAction)
+ if (positionTrackingTouch.Action is OsuAction touchAction)
{
- osuInputManager.KeyBindingContainer.TriggerReleased(directTouchAction);
+ osuInputManager.KeyBindingContainer.TriggerReleased(touchAction);
positionTrackingTouch.Action = null;
}
}
private void handleTouchMovement(TouchEvent touchEvent)
{
+ if (touchEvent is TouchMoveEvent moveEvent)
+ {
+ var trackedTouch = trackedTouches.Single(t => t.Source == touchEvent.Touch.Source);
+ trackedTouch.DistanceTravelled += moveEvent.Delta.Length;
+ }
+
// Movement should only be tracked for the most recent touch.
if (touchEvent.Touch.Source != positionTrackingTouch?.Source)
return;
@@ -148,8 +161,16 @@ namespace osu.Game.Rulesets.Osu.UI
public OsuAction? Action;
+ ///
+ /// Whether the touch was on a hit circle receptor.
+ ///
public readonly bool DirectTouch;
+ ///
+ /// The total distance on screen travelled by this touch (in local pixels).
+ ///
+ public float DistanceTravelled;
+
public TrackedTouch(TouchSource source, OsuAction? action, bool directTouch)
{
Source = source;
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs b/osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs
deleted file mode 100644
index 385ba48707..0000000000
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using Foundation;
-using osu.Framework.iOS;
-using osu.Game.Tests;
-
-namespace osu.Game.Rulesets.Taiko.Tests.iOS
-{
- [Register("AppDelegate")]
- public class AppDelegate : GameAppDelegate
- {
- protected override Framework.Game CreateGame() => new OsuTestBrowser();
- }
-}
diff --git a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs b/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
index 0e3a953728..0b6a11d8c2 100644
--- a/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
+++ b/osu.Game.Rulesets.Taiko.Tests.iOS/Application.cs
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using UIKit;
+using osu.Framework.iOS;
+using osu.Game.Tests;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
@@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, null, typeof(AppDelegate));
+ GameApplication.Main(new OsuTestBrowser());
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index 0a1f5380b5..8b1a4f688c 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Objects;
@@ -35,20 +33,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
protected override bool OnMouseDown(MouseDownEvent e)
{
- switch (e.Button)
- {
- case MouseButton.Left:
- HitObject.Type = HitType.Centre;
- EndPlacement(true);
- return true;
+ if (e.Button != MouseButton.Left)
+ return false;
- case MouseButton.Right:
- HitObject.Type = HitType.Rim;
- EndPlacement(true);
- return true;
- }
-
- return false;
+ EndPlacement(true);
+ return true;
}
public override void UpdateTimeAndPosition(SnapResult result)
diff --git a/osu.Game.Tests.iOS/AppDelegate.cs b/osu.Game.Tests.iOS/AppDelegate.cs
deleted file mode 100644
index b13027459f..0000000000
--- a/osu.Game.Tests.iOS/AppDelegate.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using Foundation;
-using osu.Framework.iOS;
-
-namespace osu.Game.Tests.iOS
-{
- [Register("AppDelegate")]
- public class AppDelegate : GameAppDelegate
- {
- protected override Framework.Game CreateGame() => new OsuTestBrowser();
- }
-}
diff --git a/osu.Game.Tests.iOS/Application.cs b/osu.Game.Tests.iOS/Application.cs
index 4678be4fb8..e5df79f3de 100644
--- a/osu.Game.Tests.iOS/Application.cs
+++ b/osu.Game.Tests.iOS/Application.cs
@@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using UIKit;
+using osu.Framework.iOS;
namespace osu.Game.Tests.iOS
{
@@ -11,7 +9,7 @@ namespace osu.Game.Tests.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, null, typeof(AppDelegate));
+ GameApplication.Main(new OsuTestBrowser());
}
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 85d304da9c..518981980b 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using NUnit.Framework;
@@ -161,6 +160,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeImageSpecifiedAsVideo()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var beatmap = decoder.Decode(stream);
+ var metadata = beatmap.Metadata;
+
+ Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
+ }
+ }
+
[Test]
public void TestDecodeBeatmapTimingPoints()
{
@@ -320,6 +334,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
var comboColors = decoder.Decode(stream).ComboColours;
+ Debug.Assert(comboColors != null);
+
Color4[] expectedColors =
{
new Color4(142, 199, 255, 255),
@@ -330,7 +346,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(255, 177, 140, 255),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
};
- Assert.AreEqual(expectedColors.Length, comboColors?.Count);
+ Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++)
Assert.AreEqual(expectedColors[i], comboColors[i]);
}
@@ -415,14 +431,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsNotNull(positionData);
Assert.IsNotNull(curveData);
- Assert.AreEqual(new Vector2(192, 168), positionData.Position);
+ Assert.AreEqual(new Vector2(192, 168), positionData!.Position);
Assert.AreEqual(956, hitObjects[0].StartTime);
Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL));
positionData = hitObjects[1] as IHasPosition;
Assert.IsNotNull(positionData);
- Assert.AreEqual(new Vector2(304, 56), positionData.Position);
+ Assert.AreEqual(new Vector2(304, 56), positionData!.Position);
Assert.AreEqual(1285, hitObjects[1].StartTime);
Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP));
}
@@ -578,8 +594,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestFallbackDecoderForCorruptedHeader()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -596,8 +612,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestFallbackDecoderForMissingHeader()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("missing-header.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -614,8 +630,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestDecodeFileWithEmptyLinesAtStart()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -632,8 +648,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestDecodeFileWithEmptyLinesAndNoHeader()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -650,8 +666,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestDecodeFileWithContentImmediatelyAfterHeader()
{
- Decoder decoder = null;
- Beatmap beatmap = null;
+ Decoder decoder = null!;
+ Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu"))
using (var stream = new LineBufferedReader(resStream))
@@ -678,7 +694,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestAllowFallbackDecoderOverwrite()
{
- Decoder decoder = null;
+ Decoder decoder = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream))
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
index 281ea4e4ff..3a776ac225 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
using NUnit.Framework;
using osuTK;
@@ -30,35 +28,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(storyboard.HasDrawable);
Assert.AreEqual(6, storyboard.Layers.Count());
- StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3);
+ StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.IsNotNull(background);
Assert.AreEqual(16, background.Elements.Count);
Assert.IsTrue(background.VisibleWhenFailing);
Assert.IsTrue(background.VisibleWhenPassing);
Assert.AreEqual("Background", background.Name);
- StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2);
+ StoryboardLayer fail = storyboard.Layers.Single(l => l.Depth == 2);
Assert.IsNotNull(fail);
Assert.AreEqual(0, fail.Elements.Count);
Assert.IsTrue(fail.VisibleWhenFailing);
Assert.IsFalse(fail.VisibleWhenPassing);
Assert.AreEqual("Fail", fail.Name);
- StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1);
+ StoryboardLayer pass = storyboard.Layers.Single(l => l.Depth == 1);
Assert.IsNotNull(pass);
Assert.AreEqual(0, pass.Elements.Count);
Assert.IsFalse(pass.VisibleWhenFailing);
Assert.IsTrue(pass.VisibleWhenPassing);
Assert.AreEqual("Pass", pass.Name);
- StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0);
+ StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0);
Assert.IsNotNull(foreground);
Assert.AreEqual(151, foreground.Elements.Count);
Assert.IsTrue(foreground.VisibleWhenFailing);
Assert.IsTrue(foreground.VisibleWhenPassing);
Assert.AreEqual("Foreground", foreground.Name);
- StoryboardLayer overlay = storyboard.Layers.FirstOrDefault(l => l.Depth == int.MinValue);
+ StoryboardLayer overlay = storyboard.Layers.Single(l => l.Depth == int.MinValue);
Assert.IsNotNull(overlay);
Assert.IsEmpty(overlay.Elements);
Assert.IsTrue(overlay.VisibleWhenFailing);
@@ -76,7 +74,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var sprite = background.Elements.ElementAt(0) as StoryboardSprite;
Assert.NotNull(sprite);
- Assert.IsTrue(sprite.HasCommands);
+ Assert.IsTrue(sprite!.HasCommands);
Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition);
Assert.IsTrue(sprite.IsDrawable);
Assert.AreEqual(Anchor.Centre, sprite.Origin);
@@ -171,6 +169,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeImageSpecifiedAsVideo()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer foreground = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(foreground.Elements.Count, Is.Zero);
+ }
+ }
+
[Test]
public void TestDecodeOutOfRangeLoopAnimationType()
{
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index adf28afc8e..a2d81c0a75 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -176,6 +176,7 @@ namespace osu.Game.Tests.Resources
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
BeatmapInfo = beatmap,
+ BeatmapHash = beatmap.Hash,
Ruleset = beatmap.Ruleset,
Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
TotalScore = 2845370,
diff --git a/osu.Game.Tests/Resources/image-specified-as-video.osb b/osu.Game.Tests/Resources/image-specified-as-video.osb
new file mode 100644
index 0000000000..9cea7dd4e7
--- /dev/null
+++ b/osu.Game.Tests/Resources/image-specified-as-video.osb
@@ -0,0 +1,4 @@
+osu file format v14
+
+[Events]
+Video,0,"BG.jpg",0,0
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index 0bd40e9962..81ebc59729 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -133,6 +133,25 @@ namespace osu.Game.Tests.Skins.IO
assertImportedOnce(import1, import2);
});
+ [Test]
+ public Task TestImportExportedNonAsciiSkinFilename() => runSkinTest(async osu =>
+ {
+ MemoryStream exportStream = new MemoryStream();
+
+ var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk"));
+ assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", osu);
+
+ import1.PerformRead(s =>
+ {
+ new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream);
+ });
+
+ string exportFilename = import1.GetDisplayString().GetValidFilename();
+
+ var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk"));
+ assertCorrectMetadata(import2, "name 『1』 [custom]", "author 1", osu);
+ });
+
[Test]
public Task TestSameMetadataNameSameFolderName([Values] bool batchImport) => runSkinTest(async osu =>
{
diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
index f1533a32b9..a5a83d7231 100644
--- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
+++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using NUnit.Framework;
@@ -51,9 +49,11 @@ namespace osu.Game.Tests.Testing
[Test]
public void TestRetrieveShader()
{
- AddAssert("ruleset shaders retrieved", () =>
- Dependencies.Get().LoadRaw(@"sh_TestVertex.vs") != null &&
- Dependencies.Get().LoadRaw(@"sh_TestFragment.fs") != null);
+ AddStep("ruleset shaders retrieved without error", () =>
+ {
+ Dependencies.Get().LoadRaw(@"sh_TestVertex.vs");
+ Dependencies.Get().LoadRaw(@"sh_TestFragment.fs");
+ });
}
[Test]
@@ -76,12 +76,12 @@ namespace osu.Game.Tests.Testing
}
public override IResourceStore CreateResourceStore() => new NamespacedResourceStore(TestResources.GetStore(), @"Resources");
- public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager();
+ public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TestRulesetConfigManager();
public override IEnumerable GetModsFor(ModType type) => Array.Empty();
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => null;
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
- public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => null!;
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
}
private class TestRulesetConfigManager : IRulesetConfigManager
diff --git a/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs b/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs
index 07427c242f..711d9ab5ea 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs
@@ -100,8 +100,10 @@ namespace osu.Game.Tests.Visual.Background
private IUniformBuffer? borderDataBuffer;
- public override void Draw(IRenderer renderer)
+ protected override void BindUniformResources(IShader shader, IRenderer renderer)
{
+ base.BindUniformResources(shader, renderer);
+
borderDataBuffer ??= renderer.CreateUniformBuffer();
borderDataBuffer.Data = borderDataBuffer.Data with
{
@@ -109,9 +111,7 @@ namespace osu.Game.Tests.Visual.Background
TexelSize = texelSize
};
- TextureShader.BindUniformBlock("m_BorderData", borderDataBuffer);
-
- base.Draw(renderer);
+ shader.BindUniformBlock("m_BorderData", borderDataBuffer);
}
protected override bool CanDrawOpaqueInterior => false;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 5442b3bfef..f3f942b74b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -35,14 +35,14 @@ namespace osu.Game.Tests.Visual.Gameplay
var referenceBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
- AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
+ AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 2));
seekTo(referenceBeatmap.Breaks[0].StartTime);
- AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting);
+ AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting.Value);
AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
- AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
+ AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
seekTo(referenceBeatmap.HitObjects[^1].GetEndTime());
AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
index 1dffeed01b..751aeb4e13 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
@@ -31,11 +31,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
- AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15);
+ AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Counters.Select(kc => kc.CountPresses.Value).Sum() == 15);
AddStep("clear results", () => Player.Results.Clear());
addSeekStep(0);
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
- AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
+ AddUntilStep("key counters reset", () => Player.HUDOverlay.KeyCounter.Counters.All(kc => kc.CountPresses.Value == 0));
AddAssert("no results triggered", () => Player.Results.Count == 0);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index b918c5e64a..eecead5415 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
- private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
+ private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single();
[BackgroundDependencyLoader]
private void load()
@@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty());
// Add any key just to display the key counter visually.
- hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
index 890ac21b40..46d5e6c4d2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
@@ -7,7 +7,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Utils;
-using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -17,28 +17,29 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public TestSceneKeyCounter()
{
- KeyCounterKeyboard testCounter;
-
- KeyCounterDisplay kc = new KeyCounterDisplay
+ KeyCounterDisplay kc = new DefaultKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Children = new KeyCounter[]
- {
- testCounter = new KeyCounterKeyboard(Key.X),
- new KeyCounterKeyboard(Key.X),
- new KeyCounterMouse(MouseButton.Left),
- new KeyCounterMouse(MouseButton.Right),
- },
};
+ kc.AddRange(new InputTrigger[]
+ {
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterMouseTrigger(MouseButton.Left),
+ new KeyCounterMouseTrigger(MouseButton.Right),
+ });
+
+ var testCounter = (DefaultKeyCounter)kc.Counters.First();
+
AddStep("Add random", () =>
{
Key key = (Key)((int)Key.A + RNG.Next(26));
- kc.Add(new KeyCounterKeyboard(key));
+ kc.Add(new KeyCounterKeyboardTrigger(key));
});
- Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key;
+ Key testKey = ((KeyCounterKeyboardTrigger)kc.Counters.First().Trigger).Key;
void addPressKeyStep()
{
@@ -46,12 +47,12 @@ namespace osu.Game.Tests.Visual.Gameplay
}
addPressKeyStep();
- AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1);
+ AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1);
addPressKeyStep();
- AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2);
- AddStep("Disable counting", () => testCounter.IsCounting = false);
+ AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2);
+ AddStep("Disable counting", () => testCounter.IsCounting.Value = false);
addPressKeyStep();
- AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2);
+ AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2);
Add(kc);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
index c476aae202..bf9b13b320 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps()
{
AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0);
- AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0));
+ AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Counters.Any(kc => kc.CountPresses.Value > 0));
AddAssert("cannot fail", () => !((ScoreAccessibleReplayPlayer)Player).AllowFail);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
index 3e415af86e..ae10207de0 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayPlayer.cs
@@ -8,6 +8,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osuTK.Input;
@@ -45,6 +46,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
}
+ [Test]
+ public void TestDoesNotFailOnExit()
+ {
+ loadPlayerWithBeatmap();
+
+ AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
+ AddAssert("ensure rank is not fail", () => Player.ScoreProcessor.Rank.Value, () => Is.Not.EqualTo(ScoreRank.F));
+ AddStep("exit player", () => Player.Exit());
+ AddUntilStep("wait for exit", () => Player.Parent == null);
+ AddAssert("ensure rank is not fail", () => Player.ScoreProcessor.Rank.Value, () => Is.Not.EqualTo(ScoreRank.F));
+ }
+
[Test]
public void TestPauseViaSpaceWithSkip()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
index 9690d00d4c..119b753d70 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
@@ -17,6 +17,8 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
+using osu.Game.Skinning.Components;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -52,6 +54,134 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for loaded", () => skinEditor.IsLoaded);
}
+ [Test]
+ public void TestDragSelection()
+ {
+ BigBlackBox box1 = null!;
+ BigBlackBox box2 = null!;
+ BigBlackBox box3 = null!;
+
+ AddStep("Add big black boxes", () =>
+ {
+ var target = Player.ChildrenOfType().First();
+ target.Add(box1 = new BigBlackBox
+ {
+ Position = new Vector2(-90),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ target.Add(box2 = new BigBlackBox
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ target.Add(box3 = new BigBlackBox
+ {
+ Position = new Vector2(90),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ });
+ });
+
+ // This step is specifically added to reproduce an edge case which was found during cyclic selection development.
+ // If everything is working as expected it should not affect the subsequent drag selections.
+ AddRepeatStep("Select top left", () =>
+ {
+ InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft + new Vector2(box1.ScreenSpaceDrawQuad.Width / 8));
+ InputManager.Click(MouseButton.Left);
+ }, 2);
+
+ AddStep("Begin drag top left", () =>
+ {
+ InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4));
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("Drag to bottom right", () =>
+ {
+ InputManager.MoveMouseTo(box3.ScreenSpaceDrawQuad.TopRight + new Vector2(-box3.ScreenSpaceDrawQuad.Width / 8, box3.ScreenSpaceDrawQuad.Height / 4));
+ });
+
+ AddStep("Release button", () =>
+ {
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("First two boxes selected", () => skinEditor.SelectedComponents, () => Is.EqualTo(new[] { box1, box2 }));
+
+ AddStep("Begin drag bottom right", () =>
+ {
+ InputManager.MoveMouseTo(box3.ScreenSpaceDrawQuad.BottomRight + new Vector2(box3.ScreenSpaceDrawQuad.Width / 4));
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("Drag to top left", () =>
+ {
+ InputManager.MoveMouseTo(box2.ScreenSpaceDrawQuad.Centre - new Vector2(box2.ScreenSpaceDrawQuad.Width / 4));
+ });
+
+ AddStep("Release button", () =>
+ {
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddAssert("Last two boxes selected", () => skinEditor.SelectedComponents, () => Is.EqualTo(new[] { box2, box3 }));
+
+ // Test cyclic selection doesn't trigger in this state.
+ AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
+ AddAssert("Last two boxes still selected", () => skinEditor.SelectedComponents, () => Is.EqualTo(new[] { box2, box3 }));
+ }
+
+ [Test]
+ public void TestCyclicSelection()
+ {
+ SkinBlueprint[] blueprints = null!;
+
+ AddStep("Add big black boxes", () =>
+ {
+ InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ InputManager.Click(MouseButton.Left);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("Three black boxes added", () => targetContainer.Components.OfType().Count(), () => Is.EqualTo(3));
+
+ AddStep("Store black box blueprints", () =>
+ {
+ blueprints = skinEditor.ChildrenOfType().Where(b => b.Item is BigBlackBox).ToArray();
+ });
+
+ AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
+
+ AddStep("move cursor to black box", () =>
+ {
+ // Slightly offset from centre to avoid random failures (see https://github.com/ppy/osu-framework/issues/5669).
+ InputManager.MoveMouseTo(((Drawable)blueprints[0].Item).ScreenSpaceDrawQuad.Centre + new Vector2(1));
+ });
+
+ AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
+ AddAssert("Selection is black box 2", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[1].Item));
+
+ AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
+ AddAssert("Selection is black box 3", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[2].Item));
+
+ AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
+ AddAssert("Selection is black box 1", () => skinEditor.SelectedComponents.Single(), () => Is.EqualTo(blueprints[0].Item));
+
+ AddStep("select all boxes", () =>
+ {
+ skinEditor.SelectedComponents.Clear();
+ skinEditor.SelectedComponents.AddRange(targetContainer.Components.OfType().Skip(1));
+ });
+
+ AddAssert("all boxes selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(2));
+ AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
+ AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
+ AddStep("click on black box stack", () => InputManager.Click(MouseButton.Left));
+ AddAssert("all boxes still selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(2));
+ }
+
[TestCase(false)]
[TestCase(true)]
public void TestBringToFront(bool alterSelectionOrder)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
index a7da8f9832..93fec60de4 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Gameplay;
using osuTK.Input;
@@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay
};
// Add any key just to display the key counter visually.
- hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
scoreProcessor.Combo.Value = 1;
return new Container
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
index 1f2329af4a..7bbfc6a62b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -18,6 +18,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Gameplay;
using osuTK.Input;
@@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
- private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First();
+ private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().Single();
[Test]
public void TestComboCounterIncrementing()
@@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay
hudOverlay = new HUDOverlay(null, Array.Empty());
// Add any key just to display the key counter visually.
- hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+ hudOverlay.KeyCounter.Add(new KeyCounterKeyboardTrigger(Key.Space));
action?.Invoke(hudOverlay);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 0d88fb01a8..283866bef2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
@@ -106,6 +107,26 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
+ [Test]
+ public void TestSaveFailedReplayWithStoryboardEndedDoesNotProgress()
+ {
+ CreateTest(() =>
+ {
+ AddStep("fail on first judgement", () => currentFailConditions = (_, _) => true);
+ AddStep("set storyboard duration to 0s", () => currentStoryboardDuration = 0);
+ });
+ AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
+ AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
+
+ AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
+ AddUntilStep("wait for button clickable", () => Player.ChildrenOfType().First().ChildrenOfType().First().Enabled.Value);
+ AddStep("click save button", () => Player.ChildrenOfType().First().ChildrenOfType().First().TriggerClick());
+
+ // Test a regression where importing the fail replay would cause progression to results screen in a failed state.
+ AddWaitStep("wait some", 10);
+ AddAssert("player is still current screen", () => Player.IsCurrentScreen());
+ }
+
[Test]
public void TestShowResultsFalse()
{
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
index aef6f9ade0..22c7bb64b2 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs
@@ -114,6 +114,19 @@ namespace osu.Game.Tests.Visual.Menus
}
}
+ [TestCase(OverlayActivation.All)]
+ [TestCase(OverlayActivation.Disabled)]
+ public void TestButtonKeyboardInputRespectsOverlayActivation(OverlayActivation mode)
+ {
+ AddStep($"set activation mode to {mode}", () => toolbar.OverlayActivationMode.Value = mode);
+ AddStep("hide toolbar", () => toolbar.Hide());
+
+ if (mode == OverlayActivation.Disabled)
+ AddAssert("check buttons not accepting input", () => InputManager.NonPositionalInputQueue.OfType().Count(), () => Is.Zero);
+ else
+ AddAssert("check buttons accepting input", () => InputManager.NonPositionalInputQueue.OfType().Count(), () => Is.Not.Zero);
+ }
+
[TestCase(OverlayActivation.All)]
[TestCase(OverlayActivation.Disabled)]
public void TestRespectsOverlayActivation(OverlayActivation mode)
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
index d937b9e6d7..224e7e411e 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs
@@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
@@ -69,10 +70,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for gameplay", () => player?.IsBreakTime.Value == false);
AddStep("press 'z'", () => InputManager.Key(Key.Z));
- AddAssert("key counter didn't increase", () => keyCounter.CountPresses == 0);
+ AddAssert("key counter didn't increase", () => keyCounter.CountPresses.Value == 0);
AddStep("press 's'", () => InputManager.Key(Key.S));
- AddAssert("key counter did increase", () => keyCounter.CountPresses == 1);
+ AddAssert("key counter did increase", () => keyCounter.CountPresses.Value == 1);
}
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index a97c8aff66..4278c46d6a 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -126,6 +126,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 13926,
TournamentId = 35,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg",
+ Image = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US@2x.jpg",
},
Badges = new[]
{
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index ef0ad6c25c..c234cc8a9c 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -84,16 +84,80 @@ namespace osu.Game.Tests.Visual.SongSelect
});
clearScores();
- checkCount(0);
+ checkDisplayedCount(0);
- loadMoreScores(() => beatmapInfo);
- checkCount(10);
+ importMoreScores(() => beatmapInfo);
+ checkDisplayedCount(10);
- loadMoreScores(() => beatmapInfo);
- checkCount(20);
+ importMoreScores(() => beatmapInfo);
+ checkDisplayedCount(20);
clearScores();
- checkCount(0);
+ checkDisplayedCount(0);
+ }
+
+ [Test]
+ public void TestLocalScoresDisplayOnBeatmapEdit()
+ {
+ BeatmapInfo beatmapInfo = null!;
+ string originalHash = string.Empty;
+
+ AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
+
+ AddStep(@"Import beatmap", () =>
+ {
+ beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
+ beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
+
+ leaderboard.BeatmapInfo = beatmapInfo;
+ });
+
+ clearScores();
+ checkDisplayedCount(0);
+
+ AddStep(@"Perform initial save to guarantee stable hash", () =>
+ {
+ IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
+ beatmapManager.Save(beatmapInfo, beatmap);
+
+ originalHash = beatmapInfo.Hash;
+ });
+
+ importMoreScores(() => beatmapInfo);
+
+ checkDisplayedCount(10);
+ checkStoredCount(10);
+
+ AddStep(@"Save with changes", () =>
+ {
+ IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
+ beatmap.Difficulty.ApproachRate = 12;
+ beatmapManager.Save(beatmapInfo, beatmap);
+ });
+
+ AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash));
+ checkDisplayedCount(0);
+ checkStoredCount(10);
+
+ importMoreScores(() => beatmapInfo);
+ importMoreScores(() => beatmapInfo);
+ checkDisplayedCount(20);
+ checkStoredCount(30);
+
+ AddStep(@"Revert changes", () =>
+ {
+ IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap;
+ beatmap.Difficulty.ApproachRate = 8;
+ beatmapManager.Save(beatmapInfo, beatmap);
+ });
+
+ AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash));
+ checkDisplayedCount(10);
+ checkStoredCount(30);
+
+ clearScores();
+ checkDisplayedCount(0);
+ checkStoredCount(0);
}
[Test]
@@ -162,9 +226,9 @@ namespace osu.Game.Tests.Visual.SongSelect
});
}
- private void loadMoreScores(Func beatmapInfo)
+ private void importMoreScores(Func beatmapInfo)
{
- AddStep(@"Load new scores via manager", () =>
+ AddStep(@"Import new scores", () =>
{
foreach (var score in generateSampleScores(beatmapInfo()))
scoreManager.Import(score);
@@ -176,8 +240,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Clear all scores", () => scoreManager.Delete());
}
- private void checkCount(int expected) =>
- AddUntilStep("Correct count displayed", () => leaderboard.ChildrenOfType().Count() == expected);
+ private void checkDisplayedCount(int expected) =>
+ AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType().Count(), () => Is.EqualTo(expected));
+
+ private void checkStoredCount(int expected) =>
+ AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All().Count(s => !s.DeletePending)), () => Is.EqualTo(expected));
private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo)
{
@@ -210,6 +277,7 @@ namespace osu.Game.Tests.Visual.SongSelect
},
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
User = new APIUser
{
Id = 6602580,
@@ -226,6 +294,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddSeconds(-30),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
{
@@ -243,6 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddSeconds(-70),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@@ -261,6 +331,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddMinutes(-40),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@@ -279,6 +350,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddHours(-2),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@@ -297,6 +369,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddHours(-25),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@@ -315,6 +388,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddHours(-50),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@@ -333,6 +407,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddHours(-72),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@@ -351,6 +426,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddMonths(-3),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@@ -369,6 +445,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Date = DateTime.Now.AddYears(-2),
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
index 316035275f..dd7bf48791 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSortTabControl.cs
@@ -14,10 +14,11 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osuTK;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
- public partial class TestSceneBeatmapListingSortTabControl : OsuTestScene
+ public partial class TestSceneBeatmapListingSortTabControl : OsuManualInputManagerTestScene
{
private readonly BeatmapListingSortTabControl control;
@@ -111,6 +112,29 @@ namespace osu.Game.Tests.Visual.UserInterface
resetUsesCriteriaOnCategory(SortCriteria.Updated, SearchCategory.Mine);
}
+ [Test]
+ public void TestSortDirectionOnCriteriaChange()
+ {
+ AddStep("set category to leaderboard", () => control.Reset(SearchCategory.Leaderboard, false));
+ AddAssert("sort direction is descending", () => control.SortDirection.Value == SortDirection.Descending);
+
+ AddStep("click ranked sort button", () =>
+ {
+ InputManager.MoveMouseTo(control.TabControl.ChildrenOfType().Single(s => s.Active.Value));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("sort direction is ascending", () => control.SortDirection.Value == SortDirection.Ascending);
+
+ AddStep("click first inactive sort button", () =>
+ {
+ InputManager.MoveMouseTo(control.TabControl.ChildrenOfType().First(s => !s.Active.Value));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("sort direction is descending", () => control.SortDirection.Value == SortDirection.Descending);
+ }
+
private void criteriaShowsOnCategory(bool expected, SortCriteria criteria, SearchCategory category)
{
AddAssert($"{criteria.ToString().ToLowerInvariant()} {(expected ? "shown" : "not shown")} on {category.ToString().ToLowerInvariant()}", () =>
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 7635c61867..529874b71e 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -94,6 +94,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
OnlineID = i,
BeatmapInfo = beatmapInfo,
+ BeatmapHash = beatmapInfo.Hash,
Accuracy = RNG.NextDouble(),
TotalScore = RNG.Next(1, 1000000),
MaxCombo = RNG.Next(1, 1000),
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 7e19cb3aa5..634cc87a9f 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -332,13 +332,6 @@ namespace osu.Game.Tournament
private void saveChanges()
{
- foreach (var r in ladder.Rounds)
- r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList();
-
- ladder.Progressions = ladder.Matches.Where(p => p.Progression.Value != null).Select(p => new TournamentProgression(p.ID, p.Progression.Value.ID)).Concat(
- ladder.Matches.Where(p => p.LosersProgression.Value != null).Select(p => new TournamentProgression(p.ID, p.LosersProgression.Value.ID, true)))
- .ToList();
-
// Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state.
string serialisedLadder = GetSerialisedLadder();
@@ -349,6 +342,13 @@ namespace osu.Game.Tournament
public string GetSerialisedLadder()
{
+ foreach (var r in ladder.Rounds)
+ r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList();
+
+ ladder.Progressions = ladder.Matches.Where(p => p.Progression.Value != null).Select(p => new TournamentProgression(p.ID, p.Progression.Value.ID)).Concat(
+ ladder.Matches.Where(p => p.LosersProgression.Value != null).Select(p => new TournamentProgression(p.ID, p.LosersProgression.Value.ID, true)))
+ .ToList();
+
return JsonConvert.SerializeObject(ladder,
new JsonSerializerSettings
{
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index eabc63b341..a9bdd21b64 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -363,6 +363,19 @@ namespace osu.Game.Beatmaps.Formats
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
break;
+ case LegacyEventType.Video:
+ string filename = CleanFilename(split[2]);
+
+ // Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
+ // instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
+ // video extensions and handle similar to a background if it doesn't match.
+ if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename)))
+ {
+ beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
+ }
+
+ break;
+
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break;
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index 44dbb3cc9f..f8308fe431 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -109,6 +109,14 @@ namespace osu.Game.Beatmaps.Formats
int offset = Parsing.ParseInt(split[1]);
string path = CleanFilename(split[2]);
+ // See handling in LegacyBeatmapDecoder for the special case where a video type is used but
+ // the file extension is not a valid video.
+ //
+ // This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
+ // (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
+ if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path)))
+ break;
+
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset));
break;
}
@@ -276,7 +284,8 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
case "A":
- timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
+ timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive,
+ startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
break;
case "H":
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 177c671bca..831e328439 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -70,8 +70,9 @@ namespace osu.Game.Database
/// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo.
/// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
/// 25 2022-09-18 Remove skins to add with new naming.
+ /// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
///
- private const int schema_version = 25;
+ private const int schema_version = 26;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
@@ -866,6 +867,15 @@ namespace osu.Game.Database
// Remove the default skins so they can be added back by SkinManager with updated naming.
migration.NewRealm.RemoveRange(migration.NewRealm.All().Where(s => s.Protected));
break;
+
+ case 26:
+ // Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap.
+ var scores = migration.NewRealm.All();
+
+ foreach (var score in scores)
+ score.BeatmapHash = score.BeatmapInfo.Hash;
+
+ break;
}
}
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index 28a715ca0b..0ee42c69d5 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -306,7 +306,7 @@ namespace osu.Game.Graphics.Backgrounds
};
shader.Bind();
- shader.BindUniformBlock("m_BorderData", borderDataBuffer);
+ shader.BindUniformBlock(@"m_BorderData", borderDataBuffer);
foreach (TriangleParticle particle in parts)
{
diff --git a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
index 6a34fefa3a..750e96440d 100644
--- a/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
+++ b/osu.Game/Graphics/Backgrounds/TrianglesV2.cs
@@ -249,7 +249,7 @@ namespace osu.Game.Graphics.Backgrounds
};
shader.Bind();
- shader.BindUniformBlock("m_BorderData", borderDataBuffer);
+ shader.BindUniformBlock(@"m_BorderData", borderDataBuffer);
Vector2 relativeSize = Vector2.Divide(triangleSize, size);
diff --git a/osu.Game/Graphics/Sprites/LogoAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs
index 220f57e9fa..f02017dc57 100644
--- a/osu.Game/Graphics/Sprites/LogoAnimation.cs
+++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs
@@ -3,13 +3,18 @@
#nullable disable
+using System;
using System.Runtime.InteropServices;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Graphics.ES30;
namespace osu.Game.Graphics.Sprites
{
@@ -18,7 +23,7 @@ namespace osu.Game.Graphics.Sprites
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
- TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation");
+ TextureShader = shaders.Load(@"LogoAnimation", @"LogoAnimation");
}
private float animationProgress;
@@ -43,11 +48,22 @@ namespace osu.Game.Graphics.Sprites
{
private LogoAnimation source => (LogoAnimation)Source;
+ private readonly Action addVertexAction;
+
private float progress;
public LogoAnimationDrawNode(LogoAnimation source)
: base(source)
{
+ addVertexAction = v =>
+ {
+ animationVertexBatch!.Add(new LogoAnimationVertex
+ {
+ Position = v.Position,
+ Colour = v.Colour,
+ TexturePosition = v.TexturePosition,
+ });
+ };
}
public override void ApplyState()
@@ -58,15 +74,34 @@ namespace osu.Game.Graphics.Sprites
}
private IUniformBuffer animationDataBuffer;
+ private IVertexBatch animationVertexBatch;
+
+ protected override void BindUniformResources(IShader shader, IRenderer renderer)
+ {
+ base.BindUniformResources(shader, renderer);
+
+ animationDataBuffer ??= renderer.CreateUniformBuffer();
+ animationVertexBatch ??= renderer.CreateQuadBatch(1, 2);
+
+ animationDataBuffer.Data = animationDataBuffer.Data with { Progress = progress };
+
+ shader.BindUniformBlock(@"m_AnimationData", animationDataBuffer);
+ }
protected override void Blit(IRenderer renderer)
{
- animationDataBuffer ??= renderer.CreateUniformBuffer();
- animationDataBuffer.Data = animationDataBuffer.Data with { Progress = progress };
-
- TextureShader.BindUniformBlock("m_AnimationData", animationDataBuffer);
+ if (DrawRectangle.Width == 0 || DrawRectangle.Height == 0)
+ return;
base.Blit(renderer);
+
+ renderer.DrawQuad(
+ Texture,
+ ScreenSpaceDrawQuad,
+ DrawColourInfo.Colour,
+ inflationPercentage: new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
+ textureCoords: TextureCoords,
+ vertexAction: addVertexAction);
}
protected override bool CanDrawOpaqueInterior => false;
@@ -83,6 +118,24 @@ namespace osu.Game.Graphics.Sprites
public UniformFloat Progress;
private readonly UniformPadding12 pad1;
}
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct LogoAnimationVertex : IEquatable, IVertex
+ {
+ [VertexMember(2, VertexAttribPointerType.Float)]
+ public Vector2 Position;
+
+ [VertexMember(4, VertexAttribPointerType.Float)]
+ public Color4 Colour;
+
+ [VertexMember(2, VertexAttribPointerType.Float)]
+ public Vector2 TexturePosition;
+
+ public readonly bool Equals(LogoAnimationVertex other) =>
+ Position.Equals(other.Position)
+ && TexturePosition.Equals(other.TexturePosition)
+ && Colour.Equals(other.Colour);
+ }
}
}
}
diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs
index 6e05929d81..422704514f 100644
--- a/osu.Game/Localisation/GraphicsSettingsStrings.cs
+++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs
@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString RendererHeader => new TranslatableString(getKey(@"renderer_header"), @"Renderer");
+ ///
+ /// "Renderer"
+ ///
+ public static LocalisableString Renderer => new TranslatableString(getKey(@"renderer"), @"Renderer");
+
///
/// "Frame limiter"
///
@@ -144,6 +149,12 @@ namespace osu.Game.Localisation
///
public static LocalisableString Png => new TranslatableString(getKey(@"png_lossless"), @"PNG (lossless)");
+ ///
+ /// "In order to change the renderer, the game will close. Please open it again."
+ ///
+ public static LocalisableString ChangeRendererConfirmation =>
+ new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again.");
+
private static string getKey(string key) => $"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs
index 6a9793b20c..5e2600bc50 100644
--- a/osu.Game/Localisation/NotificationsStrings.cs
+++ b/osu.Game/Localisation/NotificationsStrings.cs
@@ -50,16 +50,18 @@ namespace osu.Game.Localisation
public static LocalisableString NoAutoplayMod => new TranslatableString(getKey(@"no_autoplay_mod"), @"The current ruleset doesn't have an autoplay mod available!");
///
- /// "osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting."
+ /// "osu! doesn't seem to be able to play audio correctly.
+ ///
+ /// Please try changing your audio device to a working setting."
///
- public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"),
- @"osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting.");
+ public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"), @"osu! doesn't seem to be able to play audio correctly.
+
+Please try changing your audio device to a working setting.");
///
/// "The score overlay is currently disabled. You can toggle this by pressing {0}."
///
- public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"),
- @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
+ public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
private static string getKey(string key) => $@"{prefix}:{key}";
}
diff --git a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
index d2ff783413..3fa86c188c 100644
--- a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
+++ b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
@@ -65,6 +65,11 @@ namespace osu.Game.Localisation
if (manager == null)
return null;
+ // When using the English culture, prefer the fallbacks rather than osu-resources baked strings.
+ // They are guaranteed to be up-to-date, and is also what a developer expects to see when making changes to `xxxStrings.cs` files.
+ if (EffectiveCulture.Name == @"en")
+ return null;
+
try
{
return manager.GetString(key, EffectiveCulture);
diff --git a/osu.Game/Localisation/SongSelectStrings.cs b/osu.Game/Localisation/SongSelectStrings.cs
index 12f70cd967..e1ac328420 100644
--- a/osu.Game/Localisation/SongSelectStrings.cs
+++ b/osu.Game/Localisation/SongSelectStrings.cs
@@ -1,4 +1,4 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index cf58d07b9e..8f27e5dc53 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -71,7 +71,7 @@ namespace osu.Game
[Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{
- public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
+ public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
public const string OSU_PROTOCOL = "osu://";
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index 2e20f83e9e..219cbe7eef 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -209,7 +209,7 @@ namespace osu.Game.Overlays.AccountCreation
if (!string.IsNullOrEmpty(errors.Message))
passwordDescription.AddErrors(new[] { errors.Message });
- game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}");
+ game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
}
}
else
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
index 76b6dec65b..3336c383ff 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs
@@ -3,6 +3,7 @@
#nullable disable
+using osu.Framework.Graphics;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
@@ -10,8 +11,12 @@ namespace osu.Game.Overlays.BeatmapListing
{
public partial class BeatmapListingHeader : OverlayHeader
{
+ public BeatmapListingFilterControl FilterControl { get; private set; }
+
protected override OverlayTitle CreateTitle() => new BeatmapListingTitle();
+ protected override Drawable CreateContent() => FilterControl = new BeatmapListingFilterControl();
+
private partial class BeatmapListingTitle : OverlayTitle
{
public BeatmapListingTitle()
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
index 025738710f..2f290d05e9 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSortTabControl.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Overlays.BeatmapListing
if (currentParameters == null)
Reset(SearchCategory.Leaderboard, false);
+
+ Current.BindValueChanged(_ => SortDirection.Value = Overlays.SortDirection.Descending);
}
public void Reset(SearchCategory category, bool hasQuery)
@@ -102,7 +104,7 @@ namespace osu.Game.Overlays.BeatmapListing
};
}
- private partial class BeatmapTabButton : TabButton
+ public partial class BeatmapTabButton : TabButton
{
public readonly Bindable SortDirection = new Bindable();
@@ -136,7 +138,7 @@ namespace osu.Game.Overlays.BeatmapListing
SortDirection.BindValueChanged(direction =>
{
- icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown;
+ icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending && Active.Value ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown;
}, true);
}
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 73961487ed..f8784504b8 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -43,7 +43,8 @@ namespace osu.Game.Overlays
private Container panelTarget;
private FillFlowContainer foundContent;
- private BeatmapListingFilterControl filterControl;
+
+ private BeatmapListingFilterControl filterControl => Header.FilterControl;
public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue)
@@ -60,12 +61,6 @@ namespace osu.Game.Overlays
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- filterControl = new BeatmapListingFilterControl
- {
- TypingStarted = onTypingStarted,
- SearchStarted = onSearchStarted,
- SearchFinished = onSearchFinished,
- },
new Container
{
AutoSizeAxes = Axes.Y,
@@ -88,6 +83,10 @@ namespace osu.Game.Overlays
},
}
};
+
+ filterControl.TypingStarted = onTypingStarted;
+ filterControl.SearchStarted = onSearchStarted;
+ filterControl.SearchFinished = onSearchFinished;
}
protected override void LoadComplete()
diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs
index 6cfa5cb9e8..dd418a9e58 100644
--- a/osu.Game/Overlays/Comments/VotePill.cs
+++ b/osu.Game/Overlays/Comments/VotePill.cs
@@ -132,11 +132,10 @@ namespace osu.Game.Overlays.Comments
},
sideNumber = new OsuSpriteText
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreRight,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
Text = "+1",
Font = OsuFont.GetFont(size: 14),
- Margin = new MarginPadding { Right = 3 },
Alpha = 0,
},
votesCounter = new OsuSpriteText
@@ -189,7 +188,7 @@ namespace osu.Game.Overlays.Comments
else
sideNumber.FadeTo(IsHovered ? 1 : 0);
- borderContainer.BorderThickness = IsHovered ? 3 : 0;
+ borderContainer.BorderThickness = IsHovered ? 2 : 0;
}
private void onHoverAction()
diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs
index 4fdf7cb2b6..4d2c6bc9d0 100644
--- a/osu.Game/Overlays/OnlineOverlay.cs
+++ b/osu.Game/Overlays/OnlineOverlay.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -22,6 +23,7 @@ namespace osu.Game.Overlays
protected readonly OverlayScrollContainer ScrollFlow;
protected readonly LoadingLayer Loading;
+ private readonly Container loadingContainer;
private readonly Container content;
protected OnlineOverlay(OverlayColourScheme colourScheme, bool requiresSignIn = true)
@@ -65,10 +67,22 @@ namespace osu.Game.Overlays
},
}
},
- Loading = new LoadingLayer(true)
+ loadingContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = Loading = new LoadingLayer(true),
+ }
});
base.Content.Add(mainContent);
}
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ // don't block header by applying padding equal to the visible header height
+ loadingContainer.Padding = new MarginPadding { Top = Math.Max(0, Header.Height - ScrollFlow.Current) };
+ }
}
}
diff --git a/osu.Game/Overlays/OverlaySidebar.cs b/osu.Game/Overlays/OverlaySidebar.cs
index 87ce1b7e8c..b8c0032e87 100644
--- a/osu.Game/Overlays/OverlaySidebar.cs
+++ b/osu.Game/Overlays/OverlaySidebar.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays
@@ -39,7 +40,7 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = -3 }, // Compensate for scrollbar margin
- Child = new OsuScrollContainer
+ Child = new SidebarScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new Container
@@ -74,5 +75,30 @@ namespace osu.Game.Overlays
[NotNull]
protected virtual Drawable CreateContent() => Empty();
+
+ private partial class SidebarScrollContainer : OsuScrollContainer
+ {
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ if (e.ScrollDelta.Y > 0 && IsScrolledToStart())
+ return false;
+
+ if (e.ScrollDelta.Y < 0 && IsScrolledToEnd())
+ return false;
+
+ return base.OnScroll(e);
+ }
+
+ protected override bool OnDragStart(DragStartEvent e)
+ {
+ if (e.Delta.Y > 0 && IsScrolledToStart())
+ return false;
+
+ if (e.Delta.Y < 0 && IsScrolledToEnd())
+ return false;
+
+ return base.OnDragStart(e);
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs
index 8af2ab3823..5c51f5e4d0 100644
--- a/osu.Game/Overlays/OverlaySortTabControl.cs
+++ b/osu.Game/Overlays/OverlaySortTabControl.cs
@@ -117,7 +117,7 @@ namespace osu.Game.Overlays
}
}
- protected partial class TabButton : HeaderButton
+ public partial class TabButton : HeaderButton
{
public readonly BindableBool Active = new BindableBool();
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
index d2f01ef9f7..1a44262ef8 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
@@ -2,15 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
-using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
-using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Game.Resources.Localisation.Web;
using osu.Framework.Localisation;
@@ -52,36 +49,24 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
private readonly OsuSpriteText valueText;
protected readonly LinkFlowContainer DescriptionText;
- private readonly Box lineBackground;
public new int Count
{
set => valueText.Text = value.ToLocalisableString("N0");
}
- public CountSection(LocalisableString header)
+ protected CountSection(LocalisableString header)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Padding = new MarginPadding { Top = 10, Bottom = 20 };
+ Padding = new MarginPadding { Bottom = 20 };
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 5),
Children = new Drawable[]
{
- new CircularContainer
- {
- Masking = true,
- RelativeSizeAxes = Axes.X,
- Height = 2,
- Child = lineBackground = new Box
- {
- RelativeSizeAxes = Axes.Both,
- }
- },
new OsuSpriteText
{
Text = header,
@@ -91,7 +76,6 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
Text = "0",
Font = OsuFont.GetFont(size: 40, weight: FontWeight.Light),
- UseFullGlyphHeight = false,
},
DescriptionText = new LinkFlowContainer(t => t.Font = t.Font.With(size: 14))
{
@@ -101,12 +85,6 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
}
};
}
-
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
- lineBackground.Colour = colourProvider.Highlight1;
- }
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 6465d62ef0..2765d2b437 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -32,7 +32,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private FillFlowContainer> scalingSettings = null!;
private readonly Bindable currentDisplay = new Bindable();
- private readonly IBindableList windowModes = new BindableList();
private Bindable scalingMode = null!;
private Bindable sizeFullscreen = null!;
@@ -75,7 +74,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
if (window != null)
{
currentDisplay.BindTo(window.CurrentDisplayBindable);
- windowModes.BindTo(window.SupportedWindowModes);
window.DisplaysChanged += onDisplaysChanged;
}
@@ -87,7 +85,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown = new SettingsDropdown
{
LabelText = GraphicsSettingsStrings.ScreenMode,
- ItemSource = windowModes,
+ Items = window?.SupportedWindowModes,
+ CanBeShown = { Value = window?.SupportedWindowModes.Count() > 1 },
Current = config.GetBindable(FrameworkSetting.WindowMode),
},
displayDropdown = new DisplaySettingsDropdown
@@ -181,8 +180,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
updateScreenModeWarning();
}, true);
- windowModes.BindCollectionChanged((_, _) => updateDisplaySettingsVisibility());
-
currentDisplay.BindValueChanged(display => Schedule(() =>
{
resolutions.RemoveRange(1, resolutions.Count - 1);
@@ -236,7 +233,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private void updateDisplaySettingsVisibility()
{
- windowModeDropdown.CanBeShown.Value = windowModes.Count > 1;
resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen;
displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1;
safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero;
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs
index a5fdfdc105..a1f728ca87 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs
@@ -1,15 +1,18 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Linq;
+using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Configuration;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
+using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Graphics
{
@@ -17,12 +20,25 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
protected override LocalisableString Header => GraphicsSettingsStrings.RendererHeader;
+ private bool automaticRendererInUse;
+
[BackgroundDependencyLoader]
- private void load(FrameworkConfigManager config, OsuConfigManager osuConfig)
+ private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, IDialogOverlay? dialogOverlay, OsuGame? game, GameHost host)
{
- // NOTE: Compatability mode omitted
+ var renderer = config.GetBindable(FrameworkSetting.Renderer);
+ automaticRendererInUse = renderer.Value == RendererType.Automatic;
+
+ SettingsEnumDropdown rendererDropdown;
+
Children = new Drawable[]
{
+ rendererDropdown = new RendererSettingsDropdown
+ {
+ LabelText = GraphicsSettingsStrings.Renderer,
+ Current = renderer,
+ Items = host.GetPreferredRenderersForCurrentPlatform().OrderBy(t => t).Where(t => t != RendererType.Vulkan),
+ Keywords = new[] { @"compatibility", @"directx" },
+ },
// TODO: this needs to be a custom dropdown at some point
new SettingsEnumDropdown
{
@@ -41,6 +57,55 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Current = osuConfig.GetBindable(OsuSetting.ShowFpsDisplay)
},
};
+
+ renderer.BindValueChanged(r =>
+ {
+ if (r.NewValue == host.ResolvedRenderer)
+ return;
+
+ // Need to check startup renderer for the "automatic" case, as ResolvedRenderer above will track the final resolved renderer instead.
+ if (r.NewValue == RendererType.Automatic && automaticRendererInUse)
+ return;
+
+ dialogOverlay?.Push(new ConfirmDialog(GraphicsSettingsStrings.ChangeRendererConfirmation, () => game?.AttemptExit(), () =>
+ {
+ renderer.Value = automaticRendererInUse ? RendererType.Automatic : host.ResolvedRenderer;
+ }));
+ });
+
+ // TODO: remove this once we support SDL+android.
+ if (RuntimeInfo.OS == RuntimeInfo.Platform.Android)
+ {
+ rendererDropdown.Items = new[] { RendererType.Automatic, RendererType.OpenGLLegacy };
+ rendererDropdown.SetNoticeText("New renderer support for android is coming soon!", true);
+ }
+ }
+
+ private partial class RendererSettingsDropdown : SettingsEnumDropdown
+ {
+ protected override OsuDropdown CreateDropdown() => new RendererDropdown();
+
+ protected partial class RendererDropdown : DropdownControl
+ {
+ private RendererType hostResolvedRenderer;
+ private bool automaticRendererInUse;
+
+ [BackgroundDependencyLoader]
+ private void load(FrameworkConfigManager config, GameHost host)
+ {
+ var renderer = config.GetBindable(FrameworkSetting.Renderer);
+ automaticRendererInUse = renderer.Value == RendererType.Automatic;
+ hostResolvedRenderer = host.ResolvedRenderer;
+ }
+
+ protected override LocalisableString GenerateItemText(RendererType item)
+ {
+ if (item == RendererType.Automatic && automaticRendererInUse)
+ return LocalisableString.Interpolate($"{base.GenerateItemText(item)} ({hostResolvedRenderer.GetDescription()})");
+
+ return base.GenerateItemText(item);
+ }
+ }
}
}
}
diff --git a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs
index 3f8d9f80d4..db27e20010 100644
--- a/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs
+++ b/osu.Game/Overlays/SkinEditor/SkinBlueprintContainer.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Overlays.SkinEditor
[Resolved]
private SkinEditor editor { get; set; } = null!;
+ protected override bool AllowCyclicSelection => true;
+
public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer)
{
this.targetContainer = targetContainer;
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index f21ef0ee98..93294a9d30 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Toolbar
protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All);
// Toolbar and its components need keyboard input even when hidden.
- public override bool PropagateNonPositionalInputSubTree => true;
+ public override bool PropagateNonPositionalInputSubTree => OverlayActivationMode.Value != OverlayActivation.Disabled;
public Toolbar()
{
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index a7889cb98d..f8c3a730f2 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Mods
};
shader.Bind();
- shader.BindUniformBlock("m_FlashlightParameters", flashlightParametersBuffer);
+ shader.BindUniformBlock(@"m_FlashlightParameters", flashlightParametersBuffer);
renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction);
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 64fe9c8a86..4f22c0c617 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -30,6 +30,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using osuTK;
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index 96b02ee4dc..1d5fcc634e 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -25,21 +25,28 @@ namespace osu.Game.Rulesets.UI
///
/// The texture store to be used for the ruleset.
///
+ ///
+ /// Reads textures from the "Textures" folder in ruleset resources.
+ /// If not available locally, lookups will fallback to the global texture store.
+ ///
public TextureStore TextureStore { get; }
///
/// The sample store to be used for the ruleset.
///
///
- /// This is the local sample store pointing to the ruleset sample resources,
- /// the cached sample store () retrieves from
- /// this store and falls back to the parent store if this store doesn't have the requested sample.
+ /// Reads samples from the "Samples" folder in ruleset resources.
+ /// If not available locally, lookups will fallback to the global sample store.
///
public ISampleStore SampleStore { get; }
///
/// The shader manager to be used for the ruleset.
///
+ ///
+ /// Reads shaders from the "Shaders" folder in ruleset resources.
+ /// If not available locally, lookups will fallback to the global shader manager.
+ ///
public ShaderManager ShaderManager { get; }
///
@@ -61,8 +68,7 @@ namespace osu.Game.Rulesets.UI
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get()));
- ShaderManager = new ShaderManager(host.Renderer, new NamespacedResourceStore(resources, @"Shaders"));
- CacheAs(ShaderManager = new FallbackShaderManager(host.Renderer, ShaderManager, parent.Get()));
+ CacheAs(ShaderManager = new RulesetShaderManager(host.Renderer, new NamespacedResourceStore(resources, @"Shaders"), parent.Get()));
RulesetConfigManager = parent.Get().GetConfigFor(ruleset);
if (RulesetConfigManager != null)
@@ -190,24 +196,36 @@ namespace osu.Game.Rulesets.UI
}
}
- private class FallbackShaderManager : ShaderManager
+ private class RulesetShaderManager : ShaderManager
{
- private readonly ShaderManager primary;
- private readonly ShaderManager fallback;
+ private readonly ShaderManager parent;
- public FallbackShaderManager(IRenderer renderer, ShaderManager primary, ShaderManager fallback)
- : base(renderer, new ResourceStore())
+ public RulesetShaderManager(IRenderer renderer, NamespacedResourceStore rulesetResources, ShaderManager parent)
+ : base(renderer, rulesetResources)
{
- this.primary = primary;
- this.fallback = fallback;
+ this.parent = parent;
}
- public override byte[]? LoadRaw(string name) => primary.LoadRaw(name) ?? fallback.LoadRaw(name);
+ // When the debugger is attached, exceptions are expensive.
+ // Manually work around this by caching failed lookups and falling back straight to parent.
+ private readonly HashSet<(string, string)> failedLookups = new HashSet<(string, string)>();
- protected override void Dispose(bool disposing)
+ public override IShader Load(string vertex, string fragment)
{
- base.Dispose(disposing);
- if (primary.IsNotNull()) primary.Dispose();
+ if (!failedLookups.Contains((vertex, fragment)))
+ {
+ try
+ {
+ return base.Load(vertex, fragment);
+ }
+ catch
+ {
+ // Shader lookup is very non-standard. Rather than returning null on missing shaders, exceptions are thrown.
+ failedLookups.Add((vertex, fragment));
+ }
+ }
+
+ return parent.Load(vertex, fragment);
}
}
}
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 7bf0482673..2ae54a3afe 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -19,7 +19,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using static osu.Game.Input.Handlers.ReplayInputHandler;
@@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.UI
.Select(b => b.GetAction())
.Distinct()
.OrderBy(action => action)
- .Select(action => new KeyCounterAction(action)));
+ .Select(action => new KeyCounterActionTrigger(action)));
}
private partial class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler
@@ -179,11 +179,14 @@ namespace osu.Game.Rulesets.UI
{
}
- public bool OnPressed(KeyBindingPressEvent e) => Target.Children.OfType>().Any(c => c.OnPressed(e.Action, Clock.Rate >= 0));
+ public bool OnPressed(KeyBindingPressEvent e) => Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger)
+ .Select(c => (KeyCounterActionTrigger)c.Trigger)
+ .Any(c => c.OnPressed(e.Action, Clock.Rate >= 0));
public void OnReleased(KeyBindingReleaseEvent e)
{
- foreach (var c in Target.Children.OfType>())
+ foreach (var c
+ in Target.Counters.Where(c => c.Trigger is KeyCounterActionTrigger).Select(c => (KeyCounterActionTrigger)c.Trigger))
c.OnReleased(e.Action, Clock.Rate >= 0);
}
}
diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
index 6f0b0c62f8..9b145ad56e 100644
--- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
+++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
@@ -123,6 +123,7 @@ namespace osu.Game.Scoring.Legacy
// before returning for database import, we must restore the database-sourced BeatmapInfo.
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo;
+ score.ScoreInfo.BeatmapHash = workingBeatmap.BeatmapInfo.Hash;
return score;
}
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 1009474d89..02c7acf350 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -22,6 +22,9 @@ using Realms;
namespace osu.Game.Scoring
{
+ ///
+ /// A realm model containing metadata for a single score.
+ ///
[ExcludeFromDynamicCompile]
[MapTo("Score")]
public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IScoreInfo
@@ -29,8 +32,19 @@ namespace osu.Game.Scoring
[PrimaryKey]
public Guid ID { get; set; }
+ ///
+ /// The this score was made against.
+ ///
+ ///
+ /// When setting this, make sure to also set to allow relational consistency when a beatmap is potentially changed.
+ ///
public BeatmapInfo BeatmapInfo { get; set; } = null!;
+ ///
+ /// The at the point in time when the score was set.
+ ///
+ public string BeatmapHash { get; set; } = string.Empty;
+
public RulesetInfo Ruleset { get; set; } = null!;
public IList Files { get; } = null!;
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index e4e67d10d7..cb7c083d87 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -45,6 +45,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected readonly BindableList SelectedItems = new BindableList();
+ ///
+ /// Whether to allow cyclic selection on clicking multiple times.
+ ///
+ ///
+ /// Disabled by default as it does not work well with editors that support double-clicking or other advanced interactions.
+ /// Can probably be made to work with more thought.
+ ///
+ protected virtual bool AllowCyclicSelection => false;
+
protected BlueprintContainer()
{
RelativeSizeAxes = Axes.Both;
@@ -167,8 +176,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
Schedule(() =>
{
endClickSelection(e);
- clickSelectionBegan = false;
+ clickSelectionHandled = false;
isDraggingBlueprint = false;
+ wasDragStarted = false;
});
finishSelectionMovement();
@@ -182,6 +192,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
lastDragEvent = e;
+ wasDragStarted = true;
if (movementBlueprints != null)
{
@@ -339,7 +350,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
/// Whether a blueprint was selected by a previous click event.
///
- private bool clickSelectionBegan;
+ private bool clickSelectionHandled;
+
+ ///
+ /// Whether the selected blueprint(s) were already selected on mouse down. Generally used to perform selection cycling on mouse up in such a case.
+ ///
+ private bool selectedBlueprintAlreadySelectedOnMouseDown;
///
/// Attempts to select any hovered blueprints.
@@ -354,7 +370,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
if (!blueprint.IsHovered) continue;
- return clickSelectionBegan = SelectionHandler.MouseDownSelectionRequested(blueprint, e);
+ selectedBlueprintAlreadySelectedOnMouseDown = blueprint.State == SelectionState.Selected;
+ return clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e);
}
return false;
@@ -367,25 +384,48 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Whether a click selection was active.
private bool endClickSelection(MouseButtonEvent e)
{
- if (!clickSelectionBegan && !isDraggingBlueprint)
+ // If already handled a selection or drag, we don't want to perform a mouse up / click action.
+ if (clickSelectionHandled || isDraggingBlueprint) return true;
+
+ if (e.Button != MouseButton.Left) return false;
+
+ if (e.ControlPressed)
{
// if a selection didn't occur, we may want to trigger a deselection.
- if (e.ControlPressed && e.Button == MouseButton.Left)
- {
- // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
- // Priority is given to already-selected blueprints.
- foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
- {
- if (!blueprint.IsHovered) continue;
- return clickSelectionBegan = SelectionHandler.MouseUpSelectionRequested(blueprint, e);
- }
- }
+ // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
+ // Priority is given to already-selected blueprints.
+ foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Where(b => b.IsHovered).OrderByDescending(b => b.IsSelected))
+ return clickSelectionHandled = SelectionHandler.MouseUpSelectionRequested(blueprint, e);
return false;
}
- return true;
+ if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1 && AllowCyclicSelection)
+ {
+ // If a click occurred and was handled by the currently selected blueprint but didn't result in a drag,
+ // cycle between other blueprints which are also under the cursor.
+
+ // The depth of blueprints is constantly changing (see above where selected blueprints are brought to the front).
+ // For this logic, we want a stable sort order so we can correctly cycle, thus using the blueprintMap instead.
+ IEnumerable> cyclingSelectionBlueprints = blueprintMap.Values;
+
+ // If there's already a selection, let's start from the blueprint after the selection.
+ cyclingSelectionBlueprints = cyclingSelectionBlueprints.SkipWhile(b => !b.IsSelected).Skip(1);
+
+ // Add the blueprints from before the selection to the end of the enumerable to allow for cyclic selection.
+ cyclingSelectionBlueprints = cyclingSelectionBlueprints.Concat(blueprintMap.Values.TakeWhile(b => !b.IsSelected));
+
+ foreach (SelectionBlueprint blueprint in cyclingSelectionBlueprints)
+ {
+ if (!blueprint.IsHovered) continue;
+
+ // We are performing a mouse up, but selection handlers perform selection on mouse down, so we need to call that instead.
+ return clickSelectionHandled = SelectionHandler.MouseDownSelectionRequested(blueprint, e);
+ }
+ }
+
+ return false;
}
///
@@ -441,8 +481,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
private Vector2[][] movementBlueprintsOriginalPositions;
private SelectionBlueprint[] movementBlueprints;
+
+ ///
+ /// Whether a blueprint is currently being dragged.
+ ///
private bool isDraggingBlueprint;
+ ///
+ /// Whether a drag operation was started at all.
+ ///
+ private bool wasDragStarted;
+
///
/// Attempts to begin the movement of any selected blueprints.
///
@@ -454,7 +503,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Any selected blueprint that is hovered can begin the movement of the group, however only the first item (according to SortForMovement) is used for movement.
// A special case is added for when a click selection occurred before the drag
- if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
+ if (!clickSelectionHandled && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
return false;
// Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs
index b70c1f7ddf..372cfe748e 100644
--- a/osu.Game/Screens/Loader.cs
+++ b/osu.Game/Screens/Loader.cs
@@ -130,6 +130,8 @@ namespace osu.Game.Screens
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
+ loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
+
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 207b9c378b..66acd6d1b0 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -73,11 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private OsuSpriteText typeLabel = null!;
private LoadingLayer loadingLayer = null!;
- public void SelectBeatmap()
- {
- if (matchSubScreen.IsCurrentScreen())
- matchSubScreen.Push(new MultiplayerMatchSongSelect(matchSubScreen.Room));
- }
+ public void SelectBeatmap() => selectBeatmapButton.TriggerClick();
[Resolved]
private MultiplayerMatchSubScreen matchSubScreen { get; set; } = null!;
@@ -97,6 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private IDisposable? applyingSettingsOperation;
private Drawable playlistContainer = null!;
private DrawableRoomPlaylist drawablePlaylist = null!;
+ private RoundedButton selectBeatmapButton = null!;
public MatchSettings(Room room)
{
@@ -275,12 +272,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.X,
Height = DrawableRoomPlaylistItem.HEIGHT
},
- new RoundedButton
+ selectBeatmapButton = new RoundedButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
Text = "Select beatmap",
- Action = SelectBeatmap
+ Action = () =>
+ {
+ if (matchSubScreen.IsCurrentScreen())
+ matchSubScreen.Push(new MultiplayerMatchSongSelect(matchSubScreen.Room));
+ }
}
}
}
diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
similarity index 70%
rename from osu.Game/Screens/Play/KeyCounter.cs
rename to osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
index 4405542b3b..69a3e53dfc 100644
--- a/osu.Game/Screens/Play/KeyCounter.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounter.cs
@@ -1,8 +1,6 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -13,70 +11,23 @@ using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
- public abstract partial class KeyCounter : Container
+ public partial class DefaultKeyCounter : KeyCounter
{
- private Sprite buttonSprite;
- private Sprite glowSprite;
- private Container textLayer;
- private SpriteText countSpriteText;
-
- public bool IsCounting { get; set; } = true;
- private int countPresses;
-
- public int CountPresses
- {
- get => countPresses;
- private set
- {
- if (countPresses != value)
- {
- countPresses = value;
- countSpriteText.Text = value.ToString(@"#,0");
- }
- }
- }
-
- private bool isLit;
-
- public bool IsLit
- {
- get => isLit;
- protected set
- {
- if (isLit != value)
- {
- isLit = value;
- updateGlowSprite(value);
- }
- }
- }
-
- public void Increment()
- {
- if (!IsCounting)
- return;
-
- CountPresses++;
- }
-
- public void Decrement()
- {
- if (!IsCounting)
- return;
-
- CountPresses--;
- }
+ private Sprite buttonSprite = null!;
+ private Sprite glowSprite = null!;
+ private Container textLayer = null!;
+ private SpriteText countSpriteText = null!;
//further: change default values here and in KeyCounterCollection if needed, instead of passing them in every constructor
public Color4 KeyDownTextColor { get; set; } = Color4.DarkGray;
public Color4 KeyUpTextColor { get; set; } = Color4.White;
public double FadeTime { get; set; }
- protected KeyCounter(string name)
+ public DefaultKeyCounter(InputTrigger trigger)
+ : base(trigger)
{
- Name = name;
}
[BackgroundDependencyLoader(true)]
@@ -116,7 +67,7 @@ namespace osu.Game.Screens.Play
},
countSpriteText = new OsuSpriteText
{
- Text = CountPresses.ToString(@"#,0"),
+ Text = CountPresses.Value.ToString(@"#,0"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both,
@@ -130,6 +81,9 @@ namespace osu.Game.Screens.Play
// so the size can be changing between buttonSprite and glowSprite.
Height = buttonSprite.DrawHeight;
Width = buttonSprite.DrawWidth;
+
+ IsActive.BindValueChanged(e => updateGlowSprite(e.NewValue), true);
+ CountPresses.BindValueChanged(e => countSpriteText.Text = e.NewValue.ToString(@"#,0"), true);
}
private void updateGlowSprite(bool show)
diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs
new file mode 100644
index 0000000000..14d7f56093
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs
@@ -0,0 +1,83 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public partial class DefaultKeyCounterDisplay : KeyCounterDisplay
+ {
+ private const int duration = 100;
+ private const double key_fade_time = 80;
+
+ private readonly FillFlowContainer keyFlow;
+
+ public override IEnumerable Counters => keyFlow;
+
+ public DefaultKeyCounterDisplay()
+ {
+ InternalChild = keyFlow = new FillFlowContainer
+ {
+ Direction = FillDirection.Horizontal,
+ AutoSizeAxes = Axes.Both,
+ Alpha = 0,
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Don't use autosize as it will shrink to zero when KeyFlow is hidden.
+ // In turn this can cause the display to be masked off screen and never become visible again.
+ Size = keyFlow.Size;
+ }
+
+ public override void Add(InputTrigger trigger) =>
+ keyFlow.Add(new DefaultKeyCounter(trigger)
+ {
+ FadeTime = key_fade_time,
+ KeyDownTextColor = KeyDownTextColor,
+ KeyUpTextColor = KeyUpTextColor,
+ });
+
+ protected override void UpdateVisibility() =>
+ // Isolate changing visibility of the key counters from fading this component.
+ keyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
+
+ private Color4 keyDownTextColor = Color4.DarkGray;
+
+ public Color4 KeyDownTextColor
+ {
+ get => keyDownTextColor;
+ set
+ {
+ if (value != keyDownTextColor)
+ {
+ keyDownTextColor = value;
+ foreach (var child in keyFlow)
+ child.KeyDownTextColor = value;
+ }
+ }
+ }
+
+ private Color4 keyUpTextColor = Color4.White;
+
+ public Color4 KeyUpTextColor
+ {
+ get => keyUpTextColor;
+ set
+ {
+ if (value != keyUpTextColor)
+ {
+ keyUpTextColor = value;
+ foreach (var child in keyFlow)
+ child.KeyUpTextColor = value;
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/InputTrigger.cs b/osu.Game/Screens/Play/HUD/InputTrigger.cs
new file mode 100644
index 0000000000..b57f2cdf91
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/InputTrigger.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An event trigger which can be used with to create visual tracking of button/key presses.
+ ///
+ public abstract partial class InputTrigger : Component
+ {
+ ///
+ /// Callback to invoke when the associated input has been activated.
+ ///
+ /// Whether gameplay is progressing in the forward direction time-wise.
+ public delegate void OnActivateCallback(bool forwardPlayback);
+
+ ///
+ /// Callback to invoke when the associated input has been deactivated.
+ ///
+ /// Whether gameplay is progressing in the forward direction time-wise.
+ public delegate void OnDeactivateCallback(bool forwardPlayback);
+
+ public event OnActivateCallback? OnActivate;
+ public event OnDeactivateCallback? OnDeactivate;
+
+ protected InputTrigger(string name)
+ {
+ Name = name;
+ }
+
+ protected void Activate(bool forwardPlayback = true) => OnActivate?.Invoke(forwardPlayback);
+
+ protected void Deactivate(bool forwardPlayback = true) => OnDeactivate?.Invoke(forwardPlayback);
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/KeyCounter.cs b/osu.Game/Screens/Play/HUD/KeyCounter.cs
new file mode 100644
index 0000000000..2a4ab1993a
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/KeyCounter.cs
@@ -0,0 +1,98 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// An individual key display which is intended to be displayed within a .
+ ///
+ public abstract partial class KeyCounter : Container
+ {
+ ///
+ /// The which activates and deactivates this .
+ ///
+ public readonly InputTrigger Trigger;
+
+ ///
+ /// Whether the actions reported by should be counted.
+ ///
+ public Bindable IsCounting { get; } = new BindableBool(true);
+
+ private readonly Bindable countPresses = new BindableInt
+ {
+ MinValue = 0
+ };
+
+ ///
+ /// The current count of registered key presses.
+ ///
+ public IBindable CountPresses => countPresses;
+
+ private readonly Container content;
+
+ protected override Container Content => content;
+
+ ///
+ /// Whether this is currently in the "activated" state because the associated key is currently pressed.
+ ///
+ protected readonly Bindable IsActive = new BindableBool();
+
+ protected KeyCounter(InputTrigger trigger)
+ {
+ InternalChildren = new Drawable[]
+ {
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ Trigger = trigger,
+ };
+
+ Trigger.OnActivate += Activate;
+ Trigger.OnDeactivate += Deactivate;
+
+ Name = trigger.Name;
+ }
+
+ private void increment()
+ {
+ if (!IsCounting.Value)
+ return;
+
+ countPresses.Value++;
+ }
+
+ private void decrement()
+ {
+ if (!IsCounting.Value)
+ return;
+
+ countPresses.Value--;
+ }
+
+ protected virtual void Activate(bool forwardPlayback = true)
+ {
+ IsActive.Value = true;
+ if (forwardPlayback)
+ increment();
+ }
+
+ protected virtual void Deactivate(bool forwardPlayback = true)
+ {
+ IsActive.Value = false;
+ if (!forwardPlayback)
+ decrement();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ Trigger.OnActivate -= Activate;
+ Trigger.OnDeactivate -= Deactivate;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/KeyCounterAction.cs b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs
similarity index 70%
rename from osu.Game/Screens/Play/KeyCounterAction.cs
rename to osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs
index 900d9bcd0e..e5951a8bf4 100644
--- a/osu.Game/Screens/Play/KeyCounterAction.cs
+++ b/osu.Game/Screens/Play/HUD/KeyCounterActionTrigger.cs
@@ -1,18 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
- public partial class KeyCounterAction : KeyCounter
+ public partial class KeyCounterActionTrigger : InputTrigger
where T : struct
{
public T Action { get; }
- public KeyCounterAction(T action)
+ public KeyCounterActionTrigger(T action)
: base($"B{(int)(object)action + 1}")
{
Action = action;
@@ -23,9 +21,7 @@ namespace osu.Game.Screens.Play
if (!EqualityComparer.Default.Equals(action, Action))
return false;
- IsLit = true;
- if (forwards)
- Increment();
+ Activate(forwards);
return false;
}
@@ -34,9 +30,7 @@ namespace osu.Game.Screens.Play
if (!EqualityComparer.Default.Equals(action, Action))
return;
- IsLit = false;
- if (!forwards)
- Decrement();
+ Deactivate(forwards);
}
}
}
diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs
new file mode 100644
index 0000000000..49c0da6793
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs
@@ -0,0 +1,109 @@
+// Copyright (c) ppy Pty Ltd . 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Game.Configuration;
+using osuTK;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ ///
+ /// A flowing display of all gameplay keys. Individual keys can be added using implementations.
+ ///
+ public abstract partial class KeyCounterDisplay : CompositeDrawable
+ {
+ ///
+ /// Whether the key counter should be visible regardless of the configuration value.
+ /// This is true by default, but can be changed.
+ ///
+ public Bindable AlwaysVisible { get; } = new Bindable(true);
+
+ ///
+ /// The s contained in this .
+ ///
+ public abstract IEnumerable Counters { get; }
+
+ ///
+ /// Whether the actions reported by all s within this should be counted.
+ ///
+ public Bindable IsCounting { get; } = new BindableBool(true);
+
+ protected readonly Bindable ConfigVisibility = new Bindable();
+
+ protected abstract void UpdateVisibility();
+
+ private Receptor? receptor;
+
+ public void SetReceptor(Receptor receptor)
+ {
+ if (this.receptor != null)
+ throw new InvalidOperationException("Cannot set a new receptor when one is already active");
+
+ this.receptor = receptor;
+ }
+
+ ///
+ /// Add a to this display.
+ ///
+ public abstract void Add(InputTrigger trigger);
+
+ ///
+ /// Add a range of to this display.
+ ///
+ public void AddRange(IEnumerable triggers) => triggers.ForEach(Add);
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AlwaysVisible.BindValueChanged(_ => UpdateVisibility());
+ ConfigVisibility.BindValueChanged(_ => UpdateVisibility(), true);
+ }
+
+ public override bool HandleNonPositionalInput => receptor == null;
+
+ public override bool HandlePositionalInput => receptor == null;
+
+ public partial class Receptor : Drawable
+ {
+ protected readonly KeyCounterDisplay Target;
+
+ public Receptor(KeyCounterDisplay target)
+ {
+ RelativeSizeAxes = Axes.Both;
+ Depth = float.MinValue;
+ Target = target;
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
+
+ protected override bool Handle(UIEvent e)
+ {
+ switch (e)
+ {
+ case KeyDownEvent:
+ case KeyUpEvent:
+ case MouseDownEvent:
+ case MouseUpEvent:
+ return Target.InternalChildren.Any(c => c.TriggerEvent(e));
+ }
+
+ return base.Handle(e);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/KeyCounterKeyboard.cs b/osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs
similarity index 70%
rename from osu.Game/Screens/Play/KeyCounterKeyboard.cs
rename to osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs
index c5c8b7eeae..3052c1e666 100644
--- a/osu.Game/Screens/Play/KeyCounterKeyboard.cs
+++ b/osu.Game/Screens/Play/HUD/KeyCounterKeyboardTrigger.cs
@@ -1,18 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Input.Events;
using osuTK.Input;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
- public partial class KeyCounterKeyboard : KeyCounter
+ public partial class KeyCounterKeyboardTrigger : InputTrigger
{
public Key Key { get; }
- public KeyCounterKeyboard(Key key)
+ public KeyCounterKeyboardTrigger(Key key)
: base(key.ToString())
{
Key = key;
@@ -22,8 +20,7 @@ namespace osu.Game.Screens.Play
{
if (e.Key == Key)
{
- IsLit = true;
- Increment();
+ Activate();
}
return base.OnKeyDown(e);
@@ -31,7 +28,9 @@ namespace osu.Game.Screens.Play
protected override void OnKeyUp(KeyUpEvent e)
{
- if (e.Key == Key) IsLit = false;
+ if (e.Key == Key)
+ Deactivate();
+
base.OnKeyUp(e);
}
}
diff --git a/osu.Game/Screens/Play/KeyCounterMouse.cs b/osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs
similarity index 79%
rename from osu.Game/Screens/Play/KeyCounterMouse.cs
rename to osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs
index cf9c7c029f..369aaa9f74 100644
--- a/osu.Game/Screens/Play/KeyCounterMouse.cs
+++ b/osu.Game/Screens/Play/HUD/KeyCounterMouseTrigger.cs
@@ -1,19 +1,17 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Input.Events;
-using osuTK.Input;
using osuTK;
+using osuTK.Input;
-namespace osu.Game.Screens.Play
+namespace osu.Game.Screens.Play.HUD
{
- public partial class KeyCounterMouse : KeyCounter
+ public partial class KeyCounterMouseTrigger : InputTrigger
{
public MouseButton Button { get; }
- public KeyCounterMouse(MouseButton button)
+ public KeyCounterMouseTrigger(MouseButton button)
: base(getStringRepresentation(button))
{
Button = button;
@@ -39,17 +37,16 @@ namespace osu.Game.Screens.Play
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == Button)
- {
- IsLit = true;
- Increment();
- }
+ Activate();
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
- if (e.Button == Button) IsLit = false;
+ if (e.Button == Button)
+ Deactivate();
+
base.OnMouseUp(e);
}
}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index c8d06b82e8..9f050a07bd 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -331,7 +331,7 @@ namespace osu.Game.Screens.Play
ShowHealth = { BindTarget = ShowHealthBar }
};
- protected KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay
+ protected KeyCounterDisplay CreateKeyCounter() => new DefaultKeyCounterDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs
deleted file mode 100644
index bb50d4a539..0000000000
--- a/osu.Game/Screens/Play/KeyCounterDisplay.cs
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System;
-using System.Linq;
-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.Configuration;
-using osuTK;
-using osuTK.Graphics;
-
-namespace osu.Game.Screens.Play
-{
- public partial class KeyCounterDisplay : Container
- {
- private const int duration = 100;
- private const double key_fade_time = 80;
-
- private readonly Bindable configVisibility = new Bindable();
-
- protected readonly FillFlowContainer KeyFlow;
-
- protected override Container Content => KeyFlow;
-
- ///
- /// Whether the key counter should be visible regardless of the configuration value.
- /// This is true by default, but can be changed.
- ///
- public readonly Bindable AlwaysVisible = new Bindable(true);
-
- public KeyCounterDisplay()
- {
- InternalChild = KeyFlow = new FillFlowContainer
- {
- Direction = FillDirection.Horizontal,
- AutoSizeAxes = Axes.Both,
- Alpha = 0,
- };
- }
-
- protected override void Update()
- {
- base.Update();
-
- // Don't use autosize as it will shrink to zero when KeyFlow is hidden.
- // In turn this can cause the display to be masked off screen and never become visible again.
- Size = KeyFlow.Size;
- }
-
- public override void Add(KeyCounter key)
- {
- ArgumentNullException.ThrowIfNull(key);
-
- base.Add(key);
- key.IsCounting = IsCounting;
- key.FadeTime = key_fade_time;
- key.KeyDownTextColor = KeyDownTextColor;
- key.KeyUpTextColor = KeyUpTextColor;
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
- {
- config.BindWith(OsuSetting.KeyOverlay, configVisibility);
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- AlwaysVisible.BindValueChanged(_ => updateVisibility());
- configVisibility.BindValueChanged(_ => updateVisibility(), true);
- }
-
- private bool isCounting = true;
-
- public bool IsCounting
- {
- get => isCounting;
- set
- {
- if (value == isCounting) return;
-
- isCounting = value;
- foreach (var child in Children)
- child.IsCounting = value;
- }
- }
-
- private Color4 keyDownTextColor = Color4.DarkGray;
-
- public Color4 KeyDownTextColor
- {
- get => keyDownTextColor;
- set
- {
- if (value != keyDownTextColor)
- {
- keyDownTextColor = value;
- foreach (var child in Children)
- child.KeyDownTextColor = value;
- }
- }
- }
-
- private Color4 keyUpTextColor = Color4.White;
-
- public Color4 KeyUpTextColor
- {
- get => keyUpTextColor;
- set
- {
- if (value != keyUpTextColor)
- {
- keyUpTextColor = value;
- foreach (var child in Children)
- child.KeyUpTextColor = value;
- }
- }
- }
-
- private void updateVisibility() =>
- // Isolate changing visibility of the key counters from fading this component.
- KeyFlow.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
-
- public override bool HandleNonPositionalInput => receptor == null;
- public override bool HandlePositionalInput => receptor == null;
-
- private Receptor receptor;
-
- public void SetReceptor(Receptor receptor)
- {
- if (this.receptor != null)
- throw new InvalidOperationException("Cannot set a new receptor when one is already active");
-
- this.receptor = receptor;
- }
-
- public partial class Receptor : Drawable
- {
- protected readonly KeyCounterDisplay Target;
-
- public Receptor(KeyCounterDisplay target)
- {
- RelativeSizeAxes = Axes.Both;
- Depth = float.MinValue;
- Target = target;
- }
-
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
-
- protected override bool Handle(UIEvent e)
- {
- switch (e)
- {
- case KeyDownEvent:
- case KeyUpEvent:
- case MouseDownEvent:
- case MouseUpEvent:
- return Target.Children.Any(c => c.TriggerEvent(e));
- }
-
- return base.Handle(e);
- }
- }
- }
-}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index bc453d2151..5174adfc06 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -248,6 +248,7 @@ namespace osu.Game.Screens.Play
// ensure the score is in a consistent state with the current player.
Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo;
+ Score.ScoreInfo.BeatmapHash = Beatmap.Value.BeatmapInfo.Hash;
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
Score.ScoreInfo.Mods = gameplayMods;
@@ -357,14 +358,10 @@ namespace osu.Game.Screens.Play
ScoreProcessor.RevertResult(r);
};
- DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
- {
- if (storyboardEnded.NewValue)
- progressToResults(true);
- };
+ DimmableStoryboard.HasStoryboardEnded.ValueChanged += _ => checkScoreCompleted();
// Bind the judgement processors to ourselves
- ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
+ ScoreProcessor.HasCompleted.BindValueChanged(_ => checkScoreCompleted());
HealthProcessor.Failed += onFail;
// Provide judgement processors to mods after they're loaded so that they're on the gameplay clock,
@@ -440,8 +437,11 @@ namespace osu.Game.Screens.Play
},
KeyCounter =
{
+ IsCounting =
+ {
+ Value = false
+ },
AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded },
- IsCounting = false
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre
@@ -481,7 +481,7 @@ namespace osu.Game.Screens.Play
{
updateGameplayState();
updatePauseOnFocusLostState();
- HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue;
+ HUDOverlay.KeyCounter.IsCounting.Value = !isBreakTime.NewValue;
}
private void updateGameplayState()
@@ -705,19 +705,20 @@ namespace osu.Game.Screens.Play
///
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
///
- /// Thrown if this method is called more than once without changing state.
- private void scoreCompletionChanged(ValueChangedEvent completed)
+ private void checkScoreCompleted()
{
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
if (!this.IsCurrentScreen())
return;
- // Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled.
- // TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar.
- // Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
- // In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
- // but it still doesn't feel right that this exists here.
- if (!completed.NewValue)
+ // Handle cases of arriving at this method when not in a completed state.
+ // - When a storyboard completion triggered this call earlier than gameplay finishes.
+ // - When a replay has been rewound before a queued resultsDisplayDelegate has run.
+ //
+ // Currently, even if this scenario is hit, prepareAndImportScoreAsync has already been queued (and potentially run).
+ // In the scenarios above, this is a non-issue, but it still feels a bit convoluted to have to cancel in this method.
+ // Maybe this can be improved with further refactoring.
+ if (!ScoreProcessor.HasCompleted.Value)
{
resultsDisplayDelegate?.Cancel();
resultsDisplayDelegate = null;
@@ -741,12 +742,12 @@ namespace osu.Game.Screens.Play
if (!Configuration.ShowResults)
return;
- bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
+ bool storyboardStillRunning = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
- if (storyboardHasOutro)
+ // If the current beatmap has a storyboard, this method will be called again on storyboard completion.
+ // Alternatively, the user may press the outro skip button, forcing immediate display of the results screen.
+ if (storyboardStillRunning)
{
- // if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
- // or the user pressing the skip outro button.
skipOutroOverlay.Show();
return;
}
@@ -792,6 +793,8 @@ namespace osu.Game.Screens.Play
// This player instance may already be in the process of exiting.
return;
+ Debug.Assert(ScoreProcessor.Rank.Value != ScoreRank.F);
+
this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely()));
}, Time.Current + delay, 50);
@@ -1111,7 +1114,7 @@ namespace osu.Game.Screens.Play
GameplayState.HasQuit = true;
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
- if (prepareScoreForDisplayTask == null)
+ if (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null)
ScoreProcessor.FailScore(Score.ScoreInfo);
}
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 774ecc2c9c..68d3247275 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -49,6 +49,11 @@ namespace osu.Game.Screens.Select
///
public Action? BeatmapSetsChanged;
+ ///
+ /// Triggered after filter conditions have finished being applied to the model hierarchy.
+ ///
+ public Action? FilterApplied;
+
///
/// The currently selected beatmap.
///
@@ -56,6 +61,11 @@ namespace osu.Game.Screens.Select
private CarouselBeatmap? selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected);
+ ///
+ /// The total count of non-filtered beatmaps displayed.
+ ///
+ public int CountDisplayed => beatmapSets.Where(s => !s.Filtered.Value).Sum(s => s.Beatmaps.Count(b => !b.Filtered.Value));
+
///
/// The currently selected beatmap set.
///
@@ -639,6 +649,8 @@ namespace osu.Game.Screens.Select
if (alwaysResetScrollPosition || !Scroll.UserScrolling)
ScrollToSelected(true);
+
+ FilterApplied?.Invoke();
}
}
diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
index f1b773c831..a57a8b0f27 100644
--- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
+++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs
@@ -65,6 +65,7 @@ namespace osu.Game.Screens.Select.Carousel
r.All()
.Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0"
+ $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1"
+ + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
+ $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2"
+ $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmapInfo.ID, ruleset.Value.ShortName),
localScoresChanged);
diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs
index 2f21ffbe6a..38520a85b7 100644
--- a/osu.Game/Screens/Select/FilterControl.cs
+++ b/osu.Game/Screens/Select/FilterControl.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Collections;
using osu.Game.Configuration;
using osu.Game.Graphics;
@@ -27,20 +28,27 @@ namespace osu.Game.Screens.Select
{
public partial class FilterControl : Container
{
- public const float HEIGHT = 2 * side_margin + 85;
- private const float side_margin = 20;
+ public const float HEIGHT = 2 * side_margin + 120;
+
+ private const float side_margin = 10;
public Action FilterChanged;
public Bindable CurrentTextSearch => searchTextBox.Current;
+ public LocalisableString InformationalText
+ {
+ get => searchTextBox.FilterText.Text;
+ set => searchTextBox.FilterText.Text = value;
+ }
+
private OsuTabControl sortTabs;
private Bindable sortMode;
private Bindable groupMode;
- private SeekLimitedSearchTextBox searchTextBox;
+ private FilterControlTextBox searchTextBox;
private CollectionDropdown collectionDropdown;
@@ -99,72 +107,63 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.Both,
Spacing = new Vector2(0, 5),
- Children = new[]
+ Children = new Drawable[]
{
- new Container
+ searchTextBox = new FilterControlTextBox
{
RelativeSizeAxes = Axes.X,
- Height = 60,
- Children = new Drawable[]
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 1,
+ Colour = OsuColour.Gray(80),
+ },
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[]
{
- searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X },
- new Box
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.Absolute, OsuTabControl.HORIZONTAL_SPACING),
+ new Dimension(),
+ new Dimension(GridSizeMode.Absolute, OsuTabControl.HORIZONTAL_SPACING),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
+ Content = new[]
+ {
+ new[]
{
- RelativeSizeAxes = Axes.X,
- Height = 1,
- Colour = OsuColour.Gray(80),
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.BottomLeft,
- },
- new GridContainer
- {
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- ColumnDimensions = new[]
+ new OsuSpriteText
{
- new Dimension(GridSizeMode.AutoSize),
- new Dimension(GridSizeMode.Absolute, OsuTabControl.HORIZONTAL_SPACING),
- new Dimension(),
- new Dimension(GridSizeMode.Absolute, OsuTabControl.HORIZONTAL_SPACING),
- new Dimension(GridSizeMode.AutoSize),
+ Text = SortStrings.Default,
+ Font = OsuFont.GetFont(size: 14),
+ Margin = new MarginPadding(5),
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
},
- RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
- Content = new[]
+ Empty(),
+ sortTabs = new OsuTabControl
{
- new[]
- {
- new OsuSpriteText
- {
- Text = SortStrings.Default,
- Font = OsuFont.GetFont(size: 14),
- Margin = new MarginPadding(5),
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- },
- Empty(),
- sortTabs = new OsuTabControl
- {
- RelativeSizeAxes = Axes.X,
- Height = 24,
- AutoSort = true,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- AccentColour = colours.GreenLight,
- Current = { BindTarget = sortMode }
- },
- Empty(),
- new OsuTabControlCheckbox
- {
- Text = "Show converted",
- Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps),
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- },
- }
- }
- },
+ RelativeSizeAxes = Axes.X,
+ Height = 24,
+ AutoSort = true,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ AccentColour = colours.GreenLight,
+ Current = { BindTarget = sortMode }
+ },
+ Empty(),
+ new OsuTabControlCheckbox
+ {
+ Text = "Show converted",
+ Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps),
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ }
}
},
new Container
@@ -248,5 +247,33 @@ namespace osu.Game.Screens.Select
protected override bool OnClick(ClickEvent e) => true;
protected override bool OnHover(HoverEvent e) => true;
+
+ private partial class FilterControlTextBox : SeekLimitedSearchTextBox
+ {
+ private const float filter_text_size = 12;
+
+ public OsuSpriteText FilterText { get; private set; }
+
+ public FilterControlTextBox()
+ {
+ Height += filter_text_size;
+ TextContainer.Height *= (Height - filter_text_size) / Height;
+ TextContainer.Margin = new MarginPadding { Bottom = filter_text_size };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ TextContainer.Add(FilterText = new OsuSpriteText
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.TopLeft,
+ Depth = float.MinValue,
+ Font = OsuFont.Default.With(size: filter_text_size, weight: FontWeight.SemiBold),
+ Margin = new MarginPadding { Top = 2, Left = 2 },
+ Colour = colours.Yellow
+ });
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index 5c720c8491..2b40b9faf8 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -191,6 +191,7 @@ namespace osu.Game.Screens.Select.Leaderboards
scoreSubscription = realm.RegisterForNotifications(r =>
r.All().Filter($"{nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $0"
+ + $" AND {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}"
+ $" AND {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $1"
+ $" AND {nameof(ScoreInfo.DeletePending)} == false"
, beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged);
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 661eec8e97..c5e914b461 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -162,6 +162,7 @@ namespace osu.Game.Screens.Select
BleedBottom = Footer.HEIGHT,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
+ FilterApplied = updateVisibleBeatmapCount,
GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s),
}, c => carouselContainer.Child = c);
@@ -828,6 +829,7 @@ namespace osu.Game.Screens.Select
private void carouselBeatmapsLoaded()
{
bindBindables();
+ updateVisibleBeatmapCount();
Carousel.AllowSelection = true;
@@ -857,6 +859,15 @@ namespace osu.Game.Screens.Select
}
}
+ private void updateVisibleBeatmapCount()
+ {
+ FilterControl.InformationalText = Carousel.CountDisplayed == 1
+ // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918
+ // but also in this case we want support for formatting a number within a string).
+ ? $"{Carousel.CountDisplayed:#,0} matching beatmap"
+ : $"{Carousel.CountDisplayed:#,0} matching beatmaps";
+ }
+
private bool boundLocalBindables;
private void bindBindables()
diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs
index 9e2e02876d..43760c4a19 100644
--- a/osu.Game/Skinning/SkinImporter.cs
+++ b/osu.Game/Skinning/SkinImporter.cs
@@ -101,7 +101,8 @@ namespace osu.Game.Skinning
// In both of these cases, the expectation from the user is that the filename or folder name is displayed somewhere to identify the skin.
if (archiveName != item.Name
// lazer exports use this format
- && archiveName != item.GetDisplayString())
+ // GetValidFilename accounts for skins with non-ASCII characters in the name that have been exported by lazer.
+ && archiveName != item.GetDisplayString().GetValidFilename())
item.Name = @$"{item.Name} [{archiveName}]";
}
diff --git a/osu.Game/Users/TournamentBanner.cs b/osu.Game/Users/TournamentBanner.cs
index 62e1913412..e7fada1eff 100644
--- a/osu.Game/Users/TournamentBanner.cs
+++ b/osu.Game/Users/TournamentBanner.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.IO;
using Newtonsoft.Json;
namespace osu.Game.Users
@@ -17,7 +16,7 @@ namespace osu.Game.Users
[JsonProperty("image")]
public string ImageLowRes = null!;
- // TODO: remove when api returns @2x image link: https://github.com/ppy/osu-web/issues/9816
- public string Image => $@"{Path.ChangeExtension(ImageLowRes, null)}@2x{Path.GetExtension(ImageLowRes)}";
+ [JsonProperty("image@2x")]
+ public string Image = null!;
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index c08dc9ed8f..b9c6c1df9d 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,8 +36,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 8738979c57..083d8192ea 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -16,6 +16,6 @@
iossimulator-x64
-
+
diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs
deleted file mode 100644
index 1d29d59fff..0000000000
--- a/osu.iOS/AppDelegate.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System.Threading.Tasks;
-using Foundation;
-using osu.Framework.iOS;
-using UIKit;
-
-namespace osu.iOS
-{
- [Register("AppDelegate")]
- public class AppDelegate : GameAppDelegate
- {
- private OsuGameIOS game;
-
- protected override Framework.Game CreateGame() => game = new OsuGameIOS();
-
- public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
- {
- if (url.IsFileUrl)
- Task.Run(() => game.Import(url.Path));
- else
- Task.Run(() => game.HandleLink(url.AbsoluteString));
- return true;
- }
- }
-}
diff --git a/osu.iOS/Application.cs b/osu.iOS/Application.cs
index 64eb5c63f5..74bd58acb8 100644
--- a/osu.iOS/Application.cs
+++ b/osu.iOS/Application.cs
@@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using UIKit;
+using osu.Framework.iOS;
namespace osu.iOS
{
@@ -11,7 +9,7 @@ namespace osu.iOS
{
public static void Main(string[] args)
{
- UIApplication.Main(args, null, typeof(AppDelegate));
+ GameApplication.Main(new OsuGameIOS());
}
}
}
diff --git a/osu.iOS/IOSMouseSettings.cs b/osu.iOS/IOSMouseSettings.cs
deleted file mode 100644
index f464bd93b8..0000000000
--- a/osu.iOS/IOSMouseSettings.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Localisation;
-using osu.Game.Configuration;
-using osu.Game.Localisation;
-using osu.Game.Overlays.Settings;
-
-namespace osu.iOS
-{
- public partial class IOSMouseSettings : SettingsSubsection
- {
- protected override LocalisableString Header => MouseSettingsStrings.Mouse;
-
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager osuConfig)
- {
- Children = new Drawable[]
- {
- new SettingsCheckbox
- {
- LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
- TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
- Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel),
- },
- new SettingsCheckbox
- {
- LabelText = MouseSettingsStrings.DisableMouseButtons,
- Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons),
- },
- };
- }
- }
-}
diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs
index 3e79bc6ad6..c49e6907ff 100644
--- a/osu.iOS/OsuGameIOS.cs
+++ b/osu.iOS/OsuGameIOS.cs
@@ -7,10 +7,7 @@ using System;
using Foundation;
using Microsoft.Maui.Devices;
using osu.Framework.Graphics;
-using osu.Framework.Input.Handlers;
-using osu.Framework.iOS.Input;
using osu.Game;
-using osu.Game.Overlays.Settings;
using osu.Game.Updater;
using osu.Game.Utils;
@@ -29,18 +26,6 @@ namespace osu.iOS
// Because we have the home indicator (mostly) hidden we don't really care about drawing in this region.
Edges.Bottom;
- public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
- {
- switch (handler)
- {
- case IOSMouseHandler:
- return new IOSMouseSettings();
-
- default:
- return base.CreateSettingsSubsectionFor(handler);
- }
- }
-
private class IOSBatteryInfo : BatteryInfo
{
public override double? ChargeLevel => Battery.ChargeLevel;