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/Directory.Build.props b/Directory.Build.props
index 3b6b985961..734374c840 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- 9.0
+ 10.0
true
enable
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/UseLocalFramework.ps1 b/UseLocalFramework.ps1
index 837685f310..9f4547d980 100644
--- a/UseLocalFramework.ps1
+++ b/UseLocalFramework.ps1
@@ -3,15 +3,53 @@
#
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
-$CSPROJ="osu.Game/osu.Game.csproj"
+$GAME_CSPROJ="osu.Game/osu.Game.csproj"
+$ANDROID_PROPS="osu.Android.props"
+$IOS_PROPS="osu.iOS.props"
$SLN="osu.sln"
-dotnet remove $CSPROJ package ppy.osu.Framework;
-dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj;
-dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
+dotnet remove $GAME_CSPROJ reference ppy.osu.Framework;
+dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android;
+dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS;
+
+dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj `
+ ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj `
+ ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj `
+ ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj;
+
+dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj;
+dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj;
+dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj;
+
+# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files
+(Get-Content "osu.Android.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.Android.props"
+(Get-Content "osu.iOS.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.iOS.props"
+
+# needed because iOS framework nupkg includes a set of properties to work around certain issues during building,
+# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference.
+(Get-Content "osu.iOS.props") |
+ Foreach-Object {
+ if ($_ -match "")
+ {
+ " "
+ }
+
+ $_
+ } | Set-Content "osu.iOS.props"
+
+$TMP=New-TemporaryFile
$SLNF=Get-Content "osu.Desktop.slnf" | ConvertFrom-Json
-$TMP=New-TemporaryFile
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj")
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
Move-Item -Path $TMP -Destination "osu.Desktop.slnf" -Force
+
+$SLNF=Get-Content "osu.Android.slnf" | ConvertFrom-Json
+$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj")
+ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
+Move-Item -Path $TMP -Destination "osu.Android.slnf" -Force
+
+$SLNF=Get-Content "osu.iOS.slnf" | ConvertFrom-Json
+$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj")
+ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
+Move-Item -Path $TMP -Destination "osu.iOS.slnf" -Force
diff --git a/UseLocalFramework.sh b/UseLocalFramework.sh
index 4fd1fdfd1b..c12b388e96 100755
--- a/UseLocalFramework.sh
+++ b/UseLocalFramework.sh
@@ -5,14 +5,41 @@
#
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
-CSPROJ="osu.Game/osu.Game.csproj"
+GAME_CSPROJ="osu.Game/osu.Game.csproj"
+ANDROID_PROPS="osu.Android.props"
+IOS_PROPS="osu.iOS.props"
SLN="osu.sln"
-dotnet remove $CSPROJ package ppy.osu.Framework
-dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj
-dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
+dotnet remove $GAME_CSPROJ reference ppy.osu.Framework
+dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android
+dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS
+
+dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj \
+ ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj \
+ ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj \
+ ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj
+
+dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
+dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj
+dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj
+
+# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files
+sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.Android.props && rm osu.Android.props.bak
+sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.iOS.props && rm osu.iOS.props.bak
+
+# needed because iOS framework nupkg includes a set of properties to work around certain issues during building,
+# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference.
+sed -i.bak '/<\/Project>/i\
+ \
+' ./osu.iOS.props && rm osu.iOS.props.bak
-SLNF="osu.Desktop.slnf"
tmp=$(mktemp)
+
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj"]' osu.Desktop.slnf > $tmp
-mv -f $tmp $SLNF
+mv -f $tmp osu.Desktop.slnf
+
+jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj"]' osu.Android.slnf > $tmp
+mv -f $tmp osu.Android.slnf
+
+jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj"]' osu.iOS.slnf > $tmp
+mv -f $tmp osu.iOS.slnf
diff --git a/osu.Android.props b/osu.Android.props
index 234575fc62..3ede0b85da 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.Benchmarks/BenchmarkCarouselFilter.cs b/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs
new file mode 100644
index 0000000000..8f7027da17
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkCarouselFilter.cs
@@ -0,0 +1,123 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using BenchmarkDotNet.Attributes;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Carousel;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkCarouselFilter : BenchmarkTest
+ {
+ private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
+ {
+ Ruleset = new RulesetInfo
+ {
+ ShortName = "osu",
+ OnlineID = 0
+ },
+ StarRating = 4.0d,
+ Difficulty = new BeatmapDifficulty
+ {
+ ApproachRate = 5.0f,
+ DrainRate = 3.0f,
+ CircleSize = 2.0f,
+ },
+ Metadata = new BeatmapMetadata
+ {
+ Artist = "The Artist",
+ ArtistUnicode = "check unicode too",
+ Title = "Title goes here",
+ TitleUnicode = "Title goes here",
+ Author = { Username = "The Author" },
+ Source = "unit tests",
+ Tags = "look for tags too",
+ },
+ DifficultyName = "version as well",
+ Length = 2500,
+ BPM = 160,
+ BeatDivisor = 12,
+ Status = BeatmapOnlineStatus.Loved
+ };
+
+ private CarouselBeatmap carouselBeatmap = null!;
+ private FilterCriteria criteria1 = null!;
+ private FilterCriteria criteria2 = null!;
+ private FilterCriteria criteria3 = null!;
+ private FilterCriteria criteria4 = null!;
+ private FilterCriteria criteria5 = null!;
+ private FilterCriteria criteria6 = null!;
+
+ public override void SetUp()
+ {
+ var beatmap = getExampleBeatmap();
+ beatmap.OnlineID = 20201010;
+ beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 };
+ carouselBeatmap = new CarouselBeatmap(beatmap);
+ criteria1 = new FilterCriteria();
+ criteria2 = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ShortName = "catch" }
+ };
+ criteria3 = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { OnlineID = 6 },
+ AllowConvertedBeatmaps = true,
+ BPM = new FilterCriteria.OptionalRange
+ {
+ IsUpperInclusive = false,
+ Max = 160d
+ }
+ };
+ criteria4 = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { OnlineID = 6 },
+ AllowConvertedBeatmaps = true,
+ SearchText = "an artist"
+ };
+ criteria5 = new FilterCriteria
+ {
+ Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = "the author AND then something else" }
+ };
+ criteria6 = new FilterCriteria { SearchText = "20201010" };
+ }
+
+ [Benchmark]
+ public void CarouselBeatmapFilter()
+ {
+ carouselBeatmap.Filter(criteria1);
+ }
+
+ [Benchmark]
+ public void CriteriaMatchingSpecificRuleset()
+ {
+ carouselBeatmap.Filter(criteria2);
+ }
+
+ [Benchmark]
+ public void CriteriaMatchingRangeMax()
+ {
+ carouselBeatmap.Filter(criteria3);
+ }
+
+ [Benchmark]
+ public void CriteriaMatchingTerms()
+ {
+ carouselBeatmap.Filter(criteria4);
+ }
+
+ [Benchmark]
+ public void CriteriaMatchingCreator()
+ {
+ carouselBeatmap.Filter(criteria5);
+ }
+
+ [Benchmark]
+ public void CriteriaMatchingBeatmapIDs()
+ {
+ carouselBeatmap.Filter(criteria6);
+ }
+ }
+}
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/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index ea5f54a775..cd8894753f 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Catch.Edit
private void load()
{
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
- RightSideToolboxContainer.Alpha = 0;
DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
index cae19e9468..180cb98ed7 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
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/Mods/CatchModDoubleTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
index 57c06e1cd1..83db9f665b 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
index ce06b841aa..3afb8c3d89 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
index 9e38913be7..c537897439 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 1c52c092ec..f77dab56c8 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) ||
@@ -414,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI
private void clearPlate(DroppedObjectAnimation animation)
{
- var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray();
+ var caughtObjects = caughtObjectContainer.Children.ToArray();
caughtObjectContainer.Clear(false);
+ // Use the already returned PoolableDrawables for new objects
+ var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
+
droppedObjectTarget.AddRange(droppedObjects);
foreach (var droppedObject in droppedObjects)
@@ -426,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{
- var droppedObject = getDroppedObject(caughtObject);
-
caughtObjectContainer.Remove(caughtObject, false);
+ var droppedObject = getDroppedObject(caughtObject);
+
droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation);
@@ -452,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI
break;
}
+ // Define lifetime start for dropped objects to be disposed correctly when rewinding replay
+ d.LifetimeStart = Clock.CurrentTime;
d.Expire();
}
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/Mods/ManiaModDaycore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
index bec0a6a1d3..309393b664 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.5;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
index a302f95966..f4b9cf3b88 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => 1;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
index 014954dd60..8d48e3acde 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.5;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
index 4cc712060c..748725af9f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModNightcore : ModNightcore
{
- public override double ScoreMultiplier => 1;
}
}
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/Mods/TestSceneOsuModBubbles.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModBubbles.cs
new file mode 100644
index 0000000000..e72a1f79f5
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModBubbles.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public partial class TestSceneOsuModBubbles : OsuModTestScene
+ {
+ [Test]
+ public void TestOsuModBubbles() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModBubbles(),
+ Autoplay = true,
+ PassCondition = () => true
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png
new file mode 100644
index 0000000000..258162c486
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/cursor-ripple@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 907422858e..c84a6ab70f 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -18,6 +19,7 @@ using osu.Framework.Testing.Input;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
@@ -40,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable background;
+ private readonly Bindable ripples = new Bindable();
+
public TestSceneGameplayCursor()
{
var ruleset = new OsuRuleset();
@@ -57,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Tests
});
});
+ AddToggleStep("ripples", v => ripples.Value = v);
+
AddSliderStep("circle size", 0f, 10f, 0f, val =>
{
config.SetValue(OsuSetting.AutoCursorSize, true);
@@ -67,6 +73,13 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("test cursor container", () => loadContent(false));
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var rulesetConfig = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
+ rulesetConfig.BindWith(OsuRulesetSetting.ShowCursorRipples, ripples);
+ }
+
[TestCase(1, 1)]
[TestCase(5, 1)]
[TestCase(10, 1)]
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/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
index b8ad61e6dd..2056a50eda 100644
--- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SetDefault(OsuRulesetSetting.SnakingInSliders, true);
SetDefault(OsuRulesetSetting.SnakingOutSliders, true);
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
+ SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
}
}
@@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
SnakingInSliders,
SnakingOutSliders,
ShowCursorTrail,
+ ShowCursorRipples,
PlayfieldBorderStyle,
}
}
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/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 7a70257f3a..ff1e208186 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -13,8 +13,8 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Utils;
using osu.Framework.Input.Events;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@@ -62,7 +62,12 @@ namespace osu.Game.Rulesets.Osu.Edit
private void load()
{
// Give a bit of breathing room around the playfield content.
- PlayfieldContentContainer.Padding = new MarginPadding(10);
+ PlayfieldContentContainer.Padding = new MarginPadding
+ {
+ Vertical = 10,
+ Left = TOOLBOX_CONTRACTED_SIZE_LEFT + 10,
+ Right = TOOLBOX_CONTRACTED_SIZE_RIGHT + 10,
+ };
LayerBelowRuleset.AddRange(new Drawable[]
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
index 9e71f657ce..2394cf92fc 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
@@ -1,6 +1,7 @@
// 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 osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -10,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModBarrelRoll : ModBarrelRoll, IApplicableToDrawableHitObject
{
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModBubbles) };
+
public void ApplyToDrawableHitObject(DrawableHitObject d)
{
d.OnUpdate += _ =>
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs
new file mode 100644
index 0000000000..12e2090f89
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs
@@ -0,0 +1,214 @@
+// 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.Diagnostics;
+using System.Linq;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Pooling;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public partial class OsuModBubbles : Mod, IApplicableToDrawableRuleset, IApplicableToDrawableHitObject, IApplicableToScoreProcessor
+ {
+ public override string Name => "Bubbles";
+
+ public override string Acronym => "BU";
+
+ public override LocalisableString Description => "Don't let their popping distract you!";
+
+ public override double ScoreMultiplier => 1;
+
+ public override ModType Type => ModType.Fun;
+
+ // Compatibility with these seems potentially feasible in the future, blocked for now because they don't work as one would expect
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModBarrelRoll), typeof(OsuModMagnetised), typeof(OsuModRepel) };
+
+ private PlayfieldAdjustmentContainer bubbleContainer = null!;
+
+ private readonly Bindable currentCombo = new BindableInt();
+
+ private float maxSize;
+ private float bubbleSize;
+ private double bubbleFade;
+
+ private readonly DrawablePool bubblePool = new DrawablePool(100);
+
+ public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
+
+ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ {
+ currentCombo.BindTo(scoreProcessor.Combo);
+ currentCombo.BindValueChanged(combo =>
+ maxSize = Math.Min(1.75f, (float)(1.25 + 0.005 * combo.NewValue)), true);
+ }
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ // Multiplying by 2 results in an initial size that is too large, hence 1.90 has been chosen
+ // Also avoids the HitObject bleeding around the edges of the bubble drawable at minimum size
+ bubbleSize = (float)(drawableRuleset.Beatmap.HitObjects.OfType().First().Radius * 1.90f);
+ bubbleFade = drawableRuleset.Beatmap.HitObjects.OfType().First().TimePreempt * 2;
+
+ // We want to hide the judgements since they are obscured by the BubbleDrawable (due to layering)
+ drawableRuleset.Playfield.DisplayJudgements.Value = false;
+
+ bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
+
+ drawableRuleset.Overlays.Add(bubbleContainer);
+ }
+
+ public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
+ {
+ drawableObject.OnNewResult += (drawable, _) =>
+ {
+ if (drawable is not DrawableOsuHitObject drawableOsuHitObject) return;
+
+ switch (drawableOsuHitObject.HitObject)
+ {
+ case Slider:
+ case SpinnerTick:
+ break;
+
+ default:
+ addBubble();
+ break;
+ }
+
+ void addBubble()
+ {
+ BubbleDrawable bubble = bubblePool.Get();
+
+ bubble.DrawableOsuHitObject = drawableOsuHitObject;
+ bubble.InitialSize = new Vector2(bubbleSize);
+ bubble.FadeTime = bubbleFade;
+ bubble.MaxSize = maxSize;
+
+ bubbleContainer.Add(bubble);
+ }
+ };
+
+ drawableObject.OnRevertResult += (drawable, _) =>
+ {
+ if (drawable.HitObject is SpinnerTick or Slider) return;
+
+ BubbleDrawable? lastBubble = bubbleContainer.OfType().LastOrDefault();
+
+ lastBubble?.ClearTransforms();
+ lastBubble?.Expire(true);
+ };
+ }
+
+ #region Pooled Bubble drawable
+
+ private partial class BubbleDrawable : PoolableDrawable
+ {
+ public DrawableOsuHitObject? DrawableOsuHitObject { get; set; }
+
+ public Vector2 InitialSize { get; set; }
+
+ public float MaxSize { get; set; }
+
+ public double FadeTime { get; set; }
+
+ private readonly Box colourBox;
+ private readonly CircularContainer content;
+
+ public BubbleDrawable()
+ {
+ Origin = Anchor.Centre;
+ InternalChild = content = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ MaskingSmoothness = 2,
+ BorderThickness = 0,
+ BorderColour = Colour4.White,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Radius = 3,
+ Colour = Colour4.Black.Opacity(0.05f),
+ },
+ Child = colourBox = new Box { RelativeSizeAxes = Axes.Both, }
+ };
+ }
+
+ protected override void PrepareForUse()
+ {
+ Debug.Assert(DrawableOsuHitObject.IsNotNull());
+
+ Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black;
+ Scale = new Vector2(1);
+ Position = getPosition(DrawableOsuHitObject);
+ Size = InitialSize;
+
+ //We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect.
+ ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f);
+
+ // The absolute length of the bubble's animation, can be used in fractions for animations of partial length
+ double duration = 1700 + Math.Pow(FadeTime, 1.07f);
+
+ // Main bubble scaling based on combo
+ this.FadeTo(1)
+ .ScaleTo(MaxSize, duration * 0.8f)
+ .Then()
+ // Pop at the end of the bubbles life time
+ .ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint)
+ .FadeOut(duration * 0.2f, Easing.OutCirc).Expire();
+
+ if (!DrawableOsuHitObject.IsHit) return;
+
+ content.BorderThickness = InitialSize.X / 3.5f;
+ content.BorderColour = Colour4.White;
+
+ colourBox.FadeColour(colourDarker);
+
+ content.TransformTo(nameof(BorderColour), colourDarker, duration * 0.3f, Easing.OutQuint);
+ // Ripple effect utilises the border to reduce drawable count
+ content.TransformTo(nameof(BorderThickness), 2f, duration * 0.3f, Easing.OutQuint)
+ .Then()
+ // Avoids transparency overlap issues during the bubble "pop"
+ .TransformTo(nameof(BorderThickness), 0f);
+ }
+
+ private Vector2 getPosition(DrawableOsuHitObject drawableObject)
+ {
+ switch (drawableObject)
+ {
+ // SliderHeads are derived from HitCircles,
+ // so we must handle them before to avoid them using the wrong positioning logic
+ case DrawableSliderHead:
+ return drawableObject.HitObject.Position;
+
+ // Using hitobject position will cause issues with HitCircle placement due to stack leniency.
+ case DrawableHitCircle:
+ return drawableObject.Position;
+
+ default:
+ return drawableObject.HitObject.Position;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
index 371dfe6a1a..1de6b9ce55 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
index 700a3f44bc..5569df8d95 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
index 4769e7660b..bf65a6c9d3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index 38d90eb121..c8c4cd6a14 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 0.5;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel), typeof(OsuModBubbles) };
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
index b7838ebaa7..661cc948c5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
index 31a6b69d6b..28d459cedb 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Hit objects run away!";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModBubbles) };
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 48056a49de..922594a93a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -203,7 +203,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModNoScope(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed(),
- new OsuModFreezeFrame()
+ new OsuModFreezeFrame(),
+ new OsuModBubbles()
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 8fdf3821fa..52fdfea95f 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu
Cursor,
CursorTrail,
CursorParticles,
+ CursorRipple,
SliderScorePoint,
ReverseArrow,
HitCircleText,
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
index d6ce793c7e..461b4a3b45 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
@@ -98,7 +98,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
- this.FadeOut(duration, Easing.OutQuint);
+ // intentionally pile on an extra FadeOut to make it happen much faster
+ this.FadeOut(duration / 4, Easing.OutQuint);
icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 620540b8ef..f049aa088f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -100,6 +100,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
+ case OsuSkinComponents.CursorRipple:
+ if (GetTexture("cursor-ripple") != null)
+ {
+ var ripple = this.GetAnimation("cursor-ripple", false, false);
+
+ // In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
+ // If anyone complains about these not being applied, this can be uncommented.
+ //
+ // But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
+ // so we might be okay.
+ //
+ // if (ripple != null)
+ // {
+ // ripple.Scale = new Vector2(0.5f);
+ // ripple.Alpha = 0.2f;
+ // }
+
+ return ripple;
+ }
+
+ return null;
+
case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null)
return new LegacyCursorParticles();
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/Cursor/CursorRippleVisualiser.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs
new file mode 100644
index 0000000000..076d97d06a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorRippleVisualiser.cs
@@ -0,0 +1,105 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Osu.Configuration;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.UI.Cursor
+{
+ public partial class CursorRippleVisualiser : CompositeDrawable, IKeyBindingHandler
+ {
+ private readonly Bindable showRipples = new Bindable(true);
+
+ private readonly DrawablePool ripplePool = new DrawablePool(20);
+
+ public CursorRippleVisualiser()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ public Vector2 CursorScale { get; set; } = Vector2.One;
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuRulesetConfigManager? rulesetConfig)
+ {
+ rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (showRipples.Value)
+ {
+ AddInternal(ripplePool.Get(r =>
+ {
+ r.Position = e.MousePosition;
+ r.Scale = CursorScale;
+ }));
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+
+ private partial class CursorRipple : PoolableDrawable
+ {
+ private Drawable ripple = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+ Origin = Anchor.Centre;
+
+ InternalChild = ripple = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorRipple), _ => new DefaultCursorRipple())
+ {
+ Blending = BlendingParameters.Additive,
+ };
+ }
+
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
+ ClearTransforms(true);
+
+ ripple.ScaleTo(0.1f)
+ .ScaleTo(1, 700, Easing.Out);
+
+ this
+ .FadeOutFromOne(700)
+ .Expire(true);
+ }
+ }
+
+ public partial class DefaultCursorRipple : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new RingPiece(3)
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2),
+ Alpha = 0.1f,
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 9ecabd1d5e..a29faac5a0 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
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.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
@@ -255,15 +256,23 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Source.parts.CopyTo(parts, 0);
}
+ private IUniformBuffer cursorTrailParameters;
+
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
vertexBatch ??= renderer.CreateQuadBatch(max_sprites, 1);
+ cursorTrailParameters ??= renderer.CreateUniformBuffer();
+ cursorTrailParameters.Data = cursorTrailParameters.Data with
+ {
+ FadeClock = time,
+ FadeExponent = fadeExponent
+ };
+
shader.Bind();
- shader.GetUniform("g_FadeClock").UpdateValue(ref time);
- shader.GetUniform("g_FadeExponent").UpdateValue(ref fadeExponent);
+ shader.BindUniformBlock("m_CursorTrailParameters", cursorTrailParameters);
texture.Bind();
@@ -323,6 +332,15 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
base.Dispose(isDisposing);
vertexBatch?.Dispose();
+ cursorTrailParameters?.Dispose();
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ private record struct CursorTrailParameters
+ {
+ public UniformFloat FadeClock;
+ public UniformFloat FadeExponent;
+ private readonly UniformPadding8 pad1;
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index 5d7648b073..bf1ff872dd 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Bindable userCursorScale;
private Bindable autoCursorScale;
+ private readonly CursorRippleVisualiser rippleVisualiser;
+
public OsuCursorContainer()
{
InternalChild = fadeContainer = new Container
@@ -48,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Children = new[]
{
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
+ rippleVisualiser = new CursorRippleVisualiser(),
new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
}
};
@@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale;
+ rippleVisualiser.CursorScale = newScale;
cursorTrail.Scale = newScale;
}, true);
diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
index 64c4e7eef6..0e410dbf57 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs
@@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI
LabelText = RulesetSettingsStrings.CursorTrail,
Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail)
},
+ new SettingsCheckbox
+ {
+ LabelText = RulesetSettingsStrings.CursorRipples,
+ Current = config.GetBindable(OsuRulesetSetting.ShowCursorRipples)
+ },
new SettingsEnumDropdown
{
LabelText = RulesetSettingsStrings.PlayfieldBorderStyle,
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.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
index bf776fe5dd..75656e2976 100644
--- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
+++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj
@@ -4,6 +4,7 @@
Library
true
click the circles. to the beat.
+ 10
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.Tests/Mods/TestSceneTaikoModSingleTap.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs
new file mode 100644
index 0000000000..0cd3b85f8e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs
@@ -0,0 +1,212 @@
+// 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 NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Taiko.Mods;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Replays;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Mods
+{
+ public partial class TestSceneTaikoModSingleTap : TaikoModTestScene
+ {
+ [Test]
+ public void TestInputAlternate() => CreateModTest(new ModTestData
+ {
+ Mod = new TaikoModSingleTap(),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Hit
+ {
+ StartTime = 100,
+ Type = HitType.Rim
+ },
+ new Hit
+ {
+ StartTime = 300,
+ Type = HitType.Rim
+ },
+ new Hit
+ {
+ StartTime = 500,
+ Type = HitType.Rim
+ },
+ new Hit
+ {
+ StartTime = 700,
+ Type = HitType.Rim
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new TaikoReplayFrame(100, TaikoAction.RightRim),
+ new TaikoReplayFrame(120),
+ new TaikoReplayFrame(300, TaikoAction.LeftRim),
+ new TaikoReplayFrame(320),
+ new TaikoReplayFrame(500, TaikoAction.RightRim),
+ new TaikoReplayFrame(520),
+ new TaikoReplayFrame(700, TaikoAction.LeftRim),
+ new TaikoReplayFrame(720),
+ },
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1
+ });
+
+ [Test]
+ public void TestInputSameKey() => CreateModTest(new ModTestData
+ {
+ Mod = new TaikoModSingleTap(),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Hit
+ {
+ StartTime = 100,
+ Type = HitType.Rim
+ },
+ new Hit
+ {
+ StartTime = 300,
+ Type = HitType.Rim
+ },
+ new Hit
+ {
+ StartTime = 500,
+ Type = HitType.Rim
+ },
+ new Hit
+ {
+ StartTime = 700,
+ Type = HitType.Rim
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new TaikoReplayFrame(100, TaikoAction.RightRim),
+ new TaikoReplayFrame(120),
+ new TaikoReplayFrame(300, TaikoAction.RightRim),
+ new TaikoReplayFrame(320),
+ new TaikoReplayFrame(500, TaikoAction.RightRim),
+ new TaikoReplayFrame(520),
+ new TaikoReplayFrame(700, TaikoAction.RightRim),
+ new TaikoReplayFrame(720),
+ },
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 4
+ });
+
+ [Test]
+ public void TestInputIntro() => CreateModTest(new ModTestData
+ {
+ Mod = new TaikoModSingleTap(),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Hit
+ {
+ StartTime = 100,
+ Type = HitType.Rim
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new TaikoReplayFrame(0, TaikoAction.RightRim),
+ new TaikoReplayFrame(20),
+ new TaikoReplayFrame(100, TaikoAction.LeftRim),
+ new TaikoReplayFrame(120),
+ },
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
+ });
+
+ [Test]
+ public void TestInputStrong() => CreateModTest(new ModTestData
+ {
+ Mod = new TaikoModSingleTap(),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Hit
+ {
+ StartTime = 100,
+ Type = HitType.Rim
+ },
+ new Hit
+ {
+ StartTime = 300,
+ Type = HitType.Rim,
+ IsStrong = true
+ },
+ new Hit
+ {
+ StartTime = 500,
+ Type = HitType.Rim,
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new TaikoReplayFrame(100, TaikoAction.RightRim),
+ new TaikoReplayFrame(120),
+ new TaikoReplayFrame(300, TaikoAction.LeftRim),
+ new TaikoReplayFrame(320),
+ new TaikoReplayFrame(500, TaikoAction.LeftRim),
+ new TaikoReplayFrame(520),
+ },
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2
+ });
+
+ [Test]
+ public void TestInputBreaks() => CreateModTest(new ModTestData
+ {
+ Mod = new TaikoModSingleTap(),
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ Breaks = new List
+ {
+ new BreakPeriod(100, 1600),
+ },
+ HitObjects = new List
+ {
+ new Hit
+ {
+ StartTime = 100,
+ Type = HitType.Rim
+ },
+ new Hit
+ {
+ StartTime = 2000,
+ Type = HitType.Rim,
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new TaikoReplayFrame(100, TaikoAction.RightRim),
+ new TaikoReplayFrame(120),
+ // Press different key after break but before hit object.
+ new TaikoReplayFrame(1900, TaikoAction.LeftRim),
+ new TaikoReplayFrame(1820),
+ // Press original key at second hitobject and ensure it has been hit.
+ new TaikoReplayFrame(2000, TaikoAction.RightRim),
+ new TaikoReplayFrame(2020),
+ },
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs
index 7b8c8926f0..8511ad9f00 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs
@@ -73,11 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint
{
BeatLength = beat_length,
- TimeSignature = new TimeSignature(time_signature_numerator)
+ TimeSignature = new TimeSignature(time_signature_numerator),
+ OmitFirstBarLine = true
});
- beatmap.ControlPointInfo.Add(start_time, new EffectControlPoint { OmitFirstBarLine = true });
-
var barlines = new BarLineGenerator(beatmap).BarLines;
AddAssert("first barline ommited", () => barlines.All(b => b.StartTime != start_time));
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 3cc47deed0..6a35e9376b 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -72,7 +72,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.ControlPointInfo.Add(hitObject.StartTime, new EffectControlPoint
{
KiaiMode = currentEffectPoint.KiaiMode,
- OmitFirstBarLine = currentEffectPoint.OmitFirstBarLine,
ScrollSpeed = lastScrollSpeed = nextScrollSpeed,
});
}
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.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
index 4b74b4991e..1e09061859 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
@@ -1,7 +1,9 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Replays;
@@ -12,5 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
+
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
index fee0cb2744..c268087f0a 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs
@@ -1,7 +1,9 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
@@ -13,5 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
+
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index d0361b1c8d..cdeaafde10 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
- drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
+ drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
index 84aa5e6bba..f442435d9c 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDaycore : ModDaycore
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
index 89581c57bd..e517439ba4 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
index 68d6305fbf..9ef6fe8649 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs
@@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModHalfTime : ModHalfTime
{
- public override double ScoreMultiplier => 0.3;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
index 7cb14635ff..ad5da3d601 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModNightcore : ModNightcore
{
- public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs
index 69a98f8372..e90ab589fc 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs
@@ -1,6 +1,8 @@
// 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.Linq;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
@@ -9,5 +11,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public class TaikoModRelax : ModRelax
{
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus.";
+
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs
new file mode 100644
index 0000000000..a5cffca06f
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs
@@ -0,0 +1,127 @@
+// 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;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
+using osu.Game.Utils;
+using osu.Game.Rulesets.Taiko.UI;
+
+namespace osu.Game.Rulesets.Taiko.Mods
+{
+ public partial class TaikoModSingleTap : Mod, IApplicableToDrawableRuleset, IUpdatableByPlayfield
+ {
+ public override string Name => @"Single Tap";
+ public override string Acronym => @"SG";
+ public override LocalisableString Description => @"One key for dons, one key for kats.";
+
+ public override double ScoreMultiplier => 1.0;
+ public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(TaikoModCinema) };
+ public override ModType Type => ModType.Conversion;
+
+ private DrawableTaikoRuleset ruleset = null!;
+
+ private TaikoPlayfield playfield { get; set; } = null!;
+
+ private TaikoAction? lastAcceptedCentreAction { get; set; }
+ private TaikoAction? lastAcceptedRimAction { get; set; }
+
+ ///
+ /// A tracker for periods where single tap should not be enforced (i.e. non-gameplay periods).
+ ///
+ ///
+ /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time.
+ ///
+ private PeriodTracker nonGameplayPeriods = null!;
+
+ private IFrameStableClock gameplayClock = null!;
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ ruleset = (DrawableTaikoRuleset)drawableRuleset;
+ ruleset.KeyBindingInputManager.Add(new InputInterceptor(this));
+ playfield = (TaikoPlayfield)ruleset.Playfield;
+
+ var periods = new List();
+
+ if (drawableRuleset.Objects.Any())
+ {
+ periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
+
+ foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
+ periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
+
+ static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
+ }
+
+ nonGameplayPeriods = new PeriodTracker(periods);
+
+ gameplayClock = drawableRuleset.FrameStableClock;
+ }
+
+ public void Update(Playfield playfield)
+ {
+ if (!nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) return;
+
+ lastAcceptedCentreAction = null;
+ lastAcceptedRimAction = null;
+ }
+
+ private bool checkCorrectAction(TaikoAction action)
+ {
+ if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
+ return true;
+
+ // If next hit object is strong, allow usage of all actions. Strong drumrolls are ignored in this check.
+ if (playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject is TaikoStrongableHitObject hitObject
+ && hitObject.IsStrong
+ && hitObject is not DrumRoll)
+ return true;
+
+ if ((action == TaikoAction.LeftCentre || action == TaikoAction.RightCentre)
+ && (lastAcceptedCentreAction == null || lastAcceptedCentreAction == action))
+ {
+ lastAcceptedCentreAction = action;
+ return true;
+ }
+
+ if ((action == TaikoAction.LeftRim || action == TaikoAction.RightRim)
+ && (lastAcceptedRimAction == null || lastAcceptedRimAction == action))
+ {
+ lastAcceptedRimAction = action;
+ return true;
+ }
+
+ return false;
+ }
+
+ private partial class InputInterceptor : Component, IKeyBindingHandler
+ {
+ private readonly TaikoModSingleTap mod;
+
+ public InputInterceptor(TaikoModSingleTap mod)
+ {
+ this.mod = mod;
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ // if the pressed action is incorrect, block it from reaching gameplay.
+ => !mod.checkCorrectAction(e.Action);
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 4f435e73b3..a35fdb890d 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.Taiko
new TaikoModDifficultyAdjust(),
new TaikoModClassic(),
new TaikoModSwap(),
+ new TaikoModSingleTap(),
};
case ModType.Automation:
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 40203440c5..64d406a308 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -31,7 +31,9 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
- public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
+ public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
+
+ public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
@@ -67,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
- float ratio = DrawHeight / 480;
+ // Width is used because it defines how many notes fit on the playfield.
+ // We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
+ float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
@@ -90,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
- LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
+ LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 42732d90e4..3587783104 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
- private const float default_aspect = 16f / 9f;
- public readonly IBindable LockPlayfieldMaxAspect = new BindableBool(true);
+ public const float MAXIMUM_ASPECT = 16f / 9f;
+ public const float MINIMUM_ASPECT = 5f / 4f;
+
+ public readonly IBindable LockPlayfieldAspectRange = new BindableBool(true);
protected override void Update()
{
@@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI
//
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is.
- if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
- height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
+ if (LockPlayfieldAspectRange.Value)
+ {
+ float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
+ if (currentAspect > MAXIMUM_ASPECT)
+ height *= currentAspect / MAXIMUM_ASPECT;
+ else if (currentAspect < MINIMUM_ASPECT)
+ height *= currentAspect / MINIMUM_ASPECT;
+ }
+
+ // Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
+ height = Math.Min(height, 1f / 3f);
Height = height;
- // Position the taiko playfield exactly one playfield from the top of the screen.
+ // Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
+ // Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
RelativePositionAxes = Axes.Y;
Y = height;
}
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 5787bd6066..d898650b66 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,51 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeVideoWithLowercaseExtension()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.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 TestDecodeVideoWithUppercaseExtension()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.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 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()
{
@@ -181,16 +225,19 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(956, timingPoint.Time);
Assert.AreEqual(329.67032967033, timingPoint.BeatLength);
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
+ Assert.IsFalse(timingPoint.OmitFirstBarLine);
timingPoint = controlPoints.TimingPointAt(48428);
Assert.AreEqual(956, timingPoint.Time);
Assert.AreEqual(329.67032967033d, timingPoint.BeatLength);
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
+ Assert.IsFalse(timingPoint.OmitFirstBarLine);
timingPoint = controlPoints.TimingPointAt(119637);
Assert.AreEqual(119637, timingPoint.Time);
Assert.AreEqual(659.340659340659, timingPoint.BeatLength);
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
+ Assert.IsFalse(timingPoint.OmitFirstBarLine);
var difficultyPoint = controlPoints.DifficultyPointAt(0);
Assert.AreEqual(0, difficultyPoint.Time);
@@ -222,17 +269,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
var effectPoint = controlPoints.EffectPointAt(0);
Assert.AreEqual(0, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
- Assert.IsFalse(effectPoint.OmitFirstBarLine);
effectPoint = controlPoints.EffectPointAt(53703);
Assert.AreEqual(53703, effectPoint.Time);
Assert.IsTrue(effectPoint.KiaiMode);
- Assert.IsFalse(effectPoint.OmitFirstBarLine);
effectPoint = controlPoints.EffectPointAt(116637);
Assert.AreEqual(95901, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
- Assert.IsFalse(effectPoint.OmitFirstBarLine);
}
}
@@ -273,6 +317,28 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeOmitBarLineEffect()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("omit-barline-control-points.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
+
+ Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(6));
+ Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(0));
+
+ Assert.That(controlPoints.TimingPointAt(500).OmitFirstBarLine, Is.False);
+ Assert.That(controlPoints.TimingPointAt(1500).OmitFirstBarLine, Is.True);
+ Assert.That(controlPoints.TimingPointAt(2500).OmitFirstBarLine, Is.False);
+ Assert.That(controlPoints.TimingPointAt(3500).OmitFirstBarLine, Is.False);
+ Assert.That(controlPoints.TimingPointAt(4500).OmitFirstBarLine, Is.False);
+ Assert.That(controlPoints.TimingPointAt(5500).OmitFirstBarLine, Is.True);
+ }
+ }
+
[Test]
public void TestTimingPointResetsSpeedMultiplier()
{
@@ -298,6 +364,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),
@@ -308,7 +376,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]);
}
@@ -393,14 +461,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));
}
@@ -556,8 +624,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))
@@ -574,8 +642,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))
@@ -592,8 +660,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))
@@ -610,8 +678,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))
@@ -628,8 +696,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))
@@ -656,7 +724,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..34ff8bfd84 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);
@@ -97,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestLoopWithoutExplicitFadeOut()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
+ Assert.AreEqual(1, background.Elements.Count);
+
+ Assert.AreEqual(2000, background.Elements[0].StartTime);
+ Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
+
+ Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime());
+ Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay);
+ }
+ }
+
[Test]
public void TestCorrectAnimationStartTime()
{
@@ -171,6 +190,55 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ [Test]
+ public void TestDecodeVideoWithLowercaseExtension()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.EqualTo(1));
+
+ Assert.AreEqual("Video.avi", ((StoryboardVideo)video.Elements[0]).Path);
+ }
+ }
+
+ [Test]
+ public void TestDecodeVideoWithUppercaseExtension()
+ {
+ var decoder = new LegacyStoryboardDecoder();
+
+ using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var storyboard = decoder.Decode(stream);
+
+ StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.EqualTo(1));
+
+ Assert.AreEqual("Video.AVI", ((StoryboardVideo)video.Elements[0]).Path);
+ }
+ }
+
+ [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 video = storyboard.Layers.Single(l => l.Name == "Video");
+ Assert.That(video.Elements.Count, Is.Zero);
+ }
+ }
+
[Test]
public void TestDecodeOutOfRangeLoopAnimationType()
{
diff --git a/osu.Game.Tests/Database/LegacyExporterTest.cs b/osu.Game.Tests/Database/LegacyExporterTest.cs
new file mode 100644
index 0000000000..d41b3a5017
--- /dev/null
+++ b/osu.Game.Tests/Database/LegacyExporterTest.cs
@@ -0,0 +1,125 @@
+// 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 NUnit.Framework;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Platform;
+using osu.Framework.Testing;
+using osu.Game.Database;
+
+namespace osu.Game.Tests.Database
+{
+ [TestFixture]
+ public class LegacyExporterTest
+ {
+ private TestLegacyExporter legacyExporter = null!;
+ private TemporaryNativeStorage storage = null!;
+
+ private const string short_filename = "normal file name";
+
+ private const string long_filename =
+ "some file with super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name";
+
+ [SetUp]
+ public void SetUp()
+ {
+ storage = new TemporaryNativeStorage("export-storage");
+ legacyExporter = new TestLegacyExporter(storage);
+ }
+
+ [Test]
+ public void ExportFileWithNormalNameTest()
+ {
+ var item = new TestPathInfo(short_filename);
+
+ Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
+
+ exportItemAndAssert(item, short_filename);
+ }
+
+ [Test]
+ public void ExportFileWithNormalNameMultipleTimesTest()
+ {
+ var item = new TestPathInfo(short_filename);
+
+ Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
+
+ //Export multiple times
+ for (int i = 0; i < 100; i++)
+ {
+ string expectedFileName = i == 0 ? short_filename : $"{short_filename} ({i})";
+ exportItemAndAssert(item, expectedFileName);
+ }
+ }
+
+ [Test]
+ public void ExportFileWithSuperLongNameTest()
+ {
+ int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
+ string expectedName = long_filename.Remove(expectedLength);
+
+ var item = new TestPathInfo(long_filename);
+
+ Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
+ exportItemAndAssert(item, expectedName);
+ }
+
+ [Test]
+ public void ExportFileWithSuperLongNameMultipleTimesTest()
+ {
+ int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
+ string expectedName = long_filename.Remove(expectedLength);
+
+ var item = new TestPathInfo(long_filename);
+
+ Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
+
+ //Export multiple times
+ for (int i = 0; i < 100; i++)
+ {
+ string expectedFilename = i == 0 ? expectedName : $"{expectedName} ({i})";
+ exportItemAndAssert(item, expectedFilename);
+ }
+ }
+
+ private void exportItemAndAssert(IHasNamedFiles item, string expectedName)
+ {
+ Assert.DoesNotThrow(() => legacyExporter.Export(item));
+ Assert.That(storage.Exists($"exports/{expectedName}{legacyExporter.GetExtension()}"), Is.True);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (storage.IsNotNull())
+ storage.Dispose();
+ }
+
+ private class TestPathInfo : IHasNamedFiles
+ {
+ public string Filename { get; }
+
+ public IEnumerable Files { get; } = new List();
+
+ public TestPathInfo(string filename)
+ {
+ Filename = filename;
+ }
+
+ public override string ToString() => Filename;
+ }
+
+ private class TestLegacyExporter : LegacyExporter
+ {
+ public TestLegacyExporter(Storage storage)
+ : base(storage)
+ {
+ }
+
+ public string GetExtension() => FileExtension;
+
+ protected override string FileExtension => ".test";
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index 554473cf77..a2ded643fa 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -43,6 +43,18 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
+
+ cpi.Add(1200, new TimingControlPoint { OmitFirstBarLine = true }); // is not redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(3));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(3));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(3));
+
+ cpi.Add(1500, new TimingControlPoint { OmitFirstBarLine = true }); // is not redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(4));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(4));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(4));
}
[Test]
@@ -95,12 +107,12 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
- cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant
- cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant
+ cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
+ cpi.Add(1400, new EffectControlPoint { KiaiMode = true }); // is redundant
- Assert.That(cpi.Groups.Count, Is.EqualTo(2));
- Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2));
- Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(1));
+ Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
}
[Test]
diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk
new file mode 100644
index 0000000000..c85f3b6352
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk differ
diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs b/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs
index c70ad751be..99b85d0502 100644
--- a/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs
+++ b/osu.Game.Tests/Resources/Shaders/sh_TestFragment.fs
@@ -1,11 +1,14 @@
-#include "sh_Utils.h"
+#define HIGH_PRECISION_VERTEX
-varying mediump vec2 v_TexCoord;
-varying mediump vec4 v_TexRect;
+#include "sh_Utils.h"
+#include "sh_Masking.h"
+
+layout(location = 2) in highp vec2 v_TexCoord;
+
+layout(location = 0) out vec4 o_Colour;
void main(void)
{
- float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
- gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1));
+ highp float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
+ o_Colour = getRoundedColor(hsv2rgb(vec4(hueValue, 1, 1, 1)), v_TexCoord);
}
-
diff --git a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
index 4485356fa4..505554bb33 100644
--- a/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
+++ b/osu.Game.Tests/Resources/Shaders/sh_TestVertex.vs
@@ -1,31 +1,25 @@
-#include "sh_Utils.h"
+layout(location = 0) in highp vec2 m_Position;
+layout(location = 1) in lowp vec4 m_Colour;
+layout(location = 2) in highp vec2 m_TexCoord;
+layout(location = 3) in highp vec4 m_TexRect;
+layout(location = 4) in mediump vec2 m_BlendRange;
-attribute highp vec2 m_Position;
-attribute lowp vec4 m_Colour;
-attribute mediump vec2 m_TexCoord;
-attribute mediump vec4 m_TexRect;
-attribute mediump vec2 m_BlendRange;
-
-varying highp vec2 v_MaskingPosition;
-varying lowp vec4 v_Colour;
-varying mediump vec2 v_TexCoord;
-varying mediump vec4 v_TexRect;
-varying mediump vec2 v_BlendRange;
-
-uniform highp mat4 g_ProjMatrix;
-uniform highp mat3 g_ToMaskingSpace;
+layout(location = 0) out highp vec2 v_MaskingPosition;
+layout(location = 1) out lowp vec4 v_Colour;
+layout(location = 2) out highp vec2 v_TexCoord;
+layout(location = 3) out highp vec4 v_TexRect;
+layout(location = 4) out mediump vec2 v_BlendRange;
void main(void)
{
- // Transform from screen space to masking space.
- highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
- v_MaskingPosition = maskingPos.xy / maskingPos.z;
+ // Transform from screen space to masking space.
+ highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
+ v_MaskingPosition = maskingPos.xy / maskingPos.z;
- v_Colour = m_Colour;
- v_TexCoord = m_TexCoord;
- v_TexRect = m_TexRect;
- v_BlendRange = m_BlendRange;
+ v_Colour = m_Colour;
+ v_TexCoord = m_TexCoord;
+ v_TexRect = m_TexRect;
+ v_BlendRange = m_BlendRange;
- gl_Position = gProjMatrix * vec4(m_Position, 1.0, 1.0);
+ gl_Position = g_ProjMatrix * vec4(m_Position, 1.0, 1.0);
}
-
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/animation-loop-no-explicit-end-time.osb b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb
new file mode 100644
index 0000000000..7afaa445df
--- /dev/null
+++ b/osu.Game.Tests/Resources/animation-loop-no-explicit-end-time.osb
@@ -0,0 +1,6 @@
+[Events]
+//Storyboard Layer 0 (Background)
+Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
+ F,0,2000,,0,1
+ L,2000,10
+ F,18,0,1000,1,0
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/Resources/omit-barline-control-points.osu b/osu.Game.Tests/Resources/omit-barline-control-points.osu
new file mode 100644
index 0000000000..839a59215b
--- /dev/null
+++ b/osu.Game.Tests/Resources/omit-barline-control-points.osu
@@ -0,0 +1,27 @@
+osu file format v14
+
+[TimingPoints]
+
+// Uninherited: none, inherited: none
+0,500,4,2,0,100,1,0
+0,-50,4,3,0,100,0,0
+
+// Uninherited: omit, inherited: none
+1000,500,4,2,0,100,1,8
+1000,-50,4,3,0,100,0,0
+
+// Uninherited: none, inherited: omit (should be ignored, inheriting cannot omit)
+2000,500,4,2,0,100,1,0
+2000,-50,4,3,0,100,0,8
+
+// Inherited: none, uninherited: none
+3000,-50,4,3,0,100,0,0
+3000,500,4,2,0,100,1,0
+
+// Inherited: omit, uninherited: none (should be ignored, inheriting cannot omit)
+4000,-50,4,3,0,100,0,8
+4000,500,4,2,0,100,1,0
+
+// Inherited: none, uninherited: omit
+5000,-50,4,3,0,100,0,0
+5000,500,4,2,0,100,1,8
diff --git a/osu.Game.Tests/Resources/video-with-lowercase-extension.osb b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb
new file mode 100644
index 0000000000..eec09722ed
--- /dev/null
+++ b/osu.Game.Tests/Resources/video-with-lowercase-extension.osb
@@ -0,0 +1,5 @@
+osu file format v14
+
+[Events]
+0,0,"BG.jpg",0,0
+Video,0,"Video.avi",0,0
diff --git a/osu.Game.Tests/Resources/video-with-uppercase-extension.osb b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb
new file mode 100644
index 0000000000..3834a547f2
--- /dev/null
+++ b/osu.Game.Tests/Resources/video-with-uppercase-extension.osb
@@ -0,0 +1,5 @@
+osu file format v14
+
+[Events]
+0,0,"BG.jpg",0,0
+Video,0,"Video.AVI",0,0
diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
index 5cfcca303f..6639b6dd68 100644
--- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs
@@ -80,7 +80,7 @@ namespace osu.Game.Tests.Rulesets
dependencies.CacheAs(ParentTextureStore = new TestTextureStore(parent.Get().Renderer));
dependencies.CacheAs(ParentSampleStore = new TestSampleStore());
- dependencies.CacheAs(ParentShaderManager = new TestShaderManager(parent.Get().Renderer));
+ dependencies.CacheAs(ParentShaderManager = new TestShaderManager(parent.Get().Renderer, parent.Get()));
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
}
@@ -156,12 +156,15 @@ namespace osu.Game.Tests.Rulesets
private class TestShaderManager : ShaderManager
{
- public TestShaderManager(IRenderer renderer)
+ private readonly ShaderManager parentManager;
+
+ public TestShaderManager(IRenderer renderer, ShaderManager parentManager)
: base(renderer, new ResourceStore())
{
+ this.parentManager = parentManager;
}
- public override byte[] LoadRaw(string name) => null;
+ public override byte[] GetRawData(string fileName) => parentManager.GetRawData(fileName);
public bool IsDisposed { get; private set; }
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/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
index 04b8c6dd4e..bd8088cfb6 100644
--- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
+++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs
@@ -48,7 +48,9 @@ namespace osu.Game.Tests.Skins
// Covers BPM counter.
"Archives/modified-default-20221205.osk",
// Covers judgement counter.
- "Archives/modified-default-20230117.osk"
+ "Archives/modified-default-20230117.osk",
+ // Covers player avatar and flag.
+ "Archives/modified-argon-20230305.osk",
};
///
diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
index f1533a32b9..f0a9ce7beb 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,13 @@ 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().GetRawData(@"sh_TestVertex.vs");
+ Dependencies.Get().GetRawData(@"sh_TestFragment.fs");
+ Dependencies.Get().Load(@"TestVertex", @"TestFragment");
+ Dependencies.Get().Load(VertexShaderDescriptor.TEXTURE_2, @"TestFragment");
+ });
}
[Test]
@@ -76,12 +78,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/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index fbdaad1cd8..8f4250799e 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsDrawable => true;
public double StartTime => double.MinValue;
public double EndTime => double.MaxValue;
+ public double EndTimeForDisplay => double.MaxValue;
public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs b/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs
index 271642edd1..711d9ab5ea 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneTriangleBorderShader.cs
@@ -9,6 +9,7 @@ using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Rendering;
+using osu.Game.Graphics.Backgrounds;
namespace osu.Game.Tests.Visual.Background
{
@@ -97,15 +98,29 @@ namespace osu.Game.Tests.Visual.Background
texelSize = Source.texelSize;
}
- public override void Draw(IRenderer renderer)
- {
- TextureShader.GetUniform("thickness").UpdateValue(ref thickness);
- TextureShader.GetUniform("texelSize").UpdateValue(ref texelSize);
+ private IUniformBuffer? borderDataBuffer;
- base.Draw(renderer);
+ protected override void BindUniformResources(IShader shader, IRenderer renderer)
+ {
+ base.BindUniformResources(shader, renderer);
+
+ borderDataBuffer ??= renderer.CreateUniformBuffer();
+ borderDataBuffer.Data = borderDataBuffer.Data with
+ {
+ Thickness = thickness,
+ TexelSize = texelSize
+ };
+
+ shader.BindUniformBlock("m_BorderData", borderDataBuffer);
}
protected override bool CanDrawOpaqueInterior => false;
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ borderDataBuffer?.Dispose();
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index 7ab0188114..9bdb9a513c 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -108,12 +108,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick());
+ ExpandingToolboxContainer toolboxContainer = null!;
+
+ AddStep("move mouse to toolbox", () => InputManager.MoveMouseTo(toolboxContainer = hitObjectComposer.ChildrenOfType().First()));
+ AddUntilStep("toolbox is expanded", () => toolboxContainer.Expanded.Value);
+ AddUntilStep("wait for toolbox to expand", () => toolboxContainer.LatestTransformEndTime, () => Is.EqualTo(Time.Current));
+
AddStep("move mouse to overlapping toggle button", () =>
{
var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad;
- var button = hitObjectComposer
- .ChildrenOfType().First()
- .ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
+ var button = toolboxContainer.ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
InputManager.MoveMouseTo(button);
});
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.cs
new file mode 100644
index 0000000000..7f9a69833c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneLocallyModifyingOnlineBeatmaps.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 System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions;
+using osu.Game.Database;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public partial class TestSceneLocallyModifyingOnlineBeatmaps : EditorSavingTestScene
+ {
+ public override void SetUpSteps()
+ {
+ CreateInitialBeatmap = () =>
+ {
+ var importedSet = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).GetResultSafely();
+ return Game.BeatmapManager.GetWorkingBeatmap(importedSet!.Value.Beatmaps.First());
+ };
+
+ base.SetUpSteps();
+ }
+
+ [Test]
+ public void TestLocallyModifyingOnlineBeatmap()
+ {
+ AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0));
+
+ AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0));
+ SaveEditor();
+
+ ReloadEditorToSameBeatmap();
+ AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
index 709d796e97..08e036248b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs
@@ -77,5 +77,39 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("object has non-zero duration", () => EditorBeatmap.HitObjects.OfType().Single().Duration > 0);
}
+
+ [Test]
+ public void TestDisallowRepeatsOnZeroDurationObjects()
+ {
+ DragArea dragArea;
+
+ AddStep("add zero length slider", () =>
+ {
+ EditorBeatmap.Clear();
+ EditorBeatmap.Add(new Slider
+ {
+ Position = new Vector2(256, 256),
+ StartTime = 2700
+ });
+ });
+
+ AddStep("hold down drag bar", () =>
+ {
+ // distinguishes between the actual drag bar and its "underlay shadow".
+ dragArea = this.ChildrenOfType().Single(bar => bar.HandlePositionalInput);
+ InputManager.MoveMouseTo(dragArea);
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddStep("try to extend drag bar", () =>
+ {
+ var blueprint = this.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(blueprint.SelectionQuad.TopLeft + new Vector2(100, 0));
+ });
+
+ AddStep("release button", () => InputManager.PressButton(MouseButton.Left));
+
+ AddAssert("object has zero repeats", () => EditorBeatmap.HitObjects.OfType().Single().RepeatCount == 0);
+ }
}
}
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..ae46dda750 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..22f7111f68 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs
@@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -17,43 +19,63 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public TestSceneKeyCounter()
{
- KeyCounterKeyboard testCounter;
-
- KeyCounterDisplay kc = new KeyCounterDisplay
+ KeyCounterDisplay defaultDisplay = 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),
- },
+ Position = new Vector2(0, 72.7f)
};
+ KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Position = new Vector2(0, -72.7f)
+ };
+
+ defaultDisplay.AddRange(new InputTrigger[]
+ {
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterMouseTrigger(MouseButton.Left),
+ new KeyCounterMouseTrigger(MouseButton.Right),
+ });
+
+ argonDisplay.AddRange(new InputTrigger[]
+ {
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterKeyboardTrigger(Key.X),
+ new KeyCounterMouseTrigger(MouseButton.Left),
+ new KeyCounterMouseTrigger(MouseButton.Right),
+ });
+
+ var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First();
+
AddStep("Add random", () =>
{
Key key = (Key)((int)Key.A + RNG.Next(26));
- kc.Add(new KeyCounterKeyboard(key));
+ defaultDisplay.Add(new KeyCounterKeyboardTrigger(key));
+ argonDisplay.Add(new KeyCounterKeyboardTrigger(key));
});
- Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key;
+ Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key;
- void addPressKeyStep()
+ addPressKeyStep();
+ AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1);
+ addPressKeyStep();
+ AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2);
+ AddStep("Disable counting", () =>
{
- AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
- }
+ argonDisplay.IsCounting.Value = false;
+ defaultDisplay.IsCounting.Value = false;
+ });
+ addPressKeyStep();
+ AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2);
- addPressKeyStep();
- AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 1);
- addPressKeyStep();
- AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses == 2);
- AddStep("Disable counting", () => testCounter.IsCounting = false);
- addPressKeyStep();
- AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses == 2);
+ Add(defaultDisplay);
+ Add(argonDisplay);
- Add(kc);
+ void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 2ea27c2fef..dbd1ce1f6e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -45,6 +45,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private SessionStatics sessionStatics { get; set; }
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
[Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notificationOverlay;
@@ -317,6 +320,7 @@ namespace osu.Game.Tests.Visual.Gameplay
saveVolumes();
setFullVolume();
+ AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => resetPlayer(false));
@@ -333,12 +337,30 @@ namespace osu.Game.Tests.Visual.Gameplay
restoreVolumes();
}
+ [Test]
+ public void TestEpilepsyWarningWithDisabledStoryboard()
+ {
+ saveVolumes();
+ setFullVolume();
+
+ AddStep("disable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, false));
+ AddStep("change epilepsy warning", () => epilepsyWarning = true);
+ AddStep("load dummy beatmap", () => resetPlayer(false));
+
+ AddUntilStep("wait for current", () => loader.IsCurrentScreen());
+
+ AddUntilStep("epilepsy warning absent", () => getWarning() == null);
+
+ restoreVolumes();
+ }
+
[Test]
public void TestEpilepsyWarningEarlyExit()
{
saveVolumes();
setFullVolume();
+ AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false));
@@ -449,7 +471,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("click notification", () => notification.TriggerClick());
}
- private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault();
+ private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault(w => w.IsAlive);
private partial class TestPlayerLoader : PlayerLoader
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
index 0469df1de3..d16f51f36e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs
@@ -164,6 +164,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
}
+ [Test]
+ public void TestRevertNestedObjects()
+ {
+ ManualClock clock = null;
+
+ var beatmap = new Beatmap();
+ beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 });
+
+ createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
+
+ AddStep("skip to middle of object", () => clock.CurrentTime = (beatmap.HitObjects[0].StartTime + beatmap.HitObjects[0].GetEndTime()) / 2);
+ AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2));
+
+ AddStep("skip to before end of object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() - 1);
+ AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
+
+ DrawableHitObject drawableHitObject = null;
+ HashSet revertedHitObjects = new HashSet();
+
+ AddStep("retrieve drawable hit object", () => drawableHitObject = playfield.ChildrenOfType().Single());
+ AddStep("set up revert tracking", () =>
+ {
+ revertedHitObjects.Clear();
+ drawableHitObject.OnRevertResult += (ho, _) => revertedHitObjects.Add(ho.HitObject);
+ });
+ AddStep("skip back to object start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime);
+ AddAssert("3 reverts fired", () => revertedHitObjects, () => Has.Count.EqualTo(3));
+ AddAssert("no objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
+ }
+
[Test]
public void TestApplyHitResultOnKilled()
{
@@ -258,6 +288,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
RegisterPool(poolSize);
RegisterPool(poolSize);
+ RegisterPool(poolSize);
+ RegisterPool(poolSize);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
@@ -388,6 +420,120 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
+ private class TestHitObjectWithNested : TestHitObject
+ {
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ base.CreateNestedHitObjects(cancellationToken);
+
+ for (int i = 0; i < 3; ++i)
+ AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 });
+ }
+ }
+
+ private class NestedHitObject : ConvertHitObject
+ {
+ }
+
+ private partial class DrawableTestHitObjectWithNested : DrawableHitObject
+ {
+ private Container nestedContainer;
+
+ public DrawableTestHitObjectWithNested()
+ : base(null)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddRangeInternal(new Drawable[]
+ {
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.Red
+ },
+ nestedContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ Size = new Vector2(200, 50);
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+ nestedContainer.Add(hitObject);
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ nestedContainer.Clear(false);
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ base.CheckForResult(userTriggered, timeOffset);
+ if (timeOffset >= 0)
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ }
+ }
+
+ private partial class DrawableNestedHitObject : DrawableHitObject
+ {
+ public DrawableNestedHitObject()
+ : this(null)
+ {
+ }
+
+ public DrawableNestedHitObject(NestedHitObject hitObject)
+ : base(hitObject)
+ {
+ Size = new Vector2(15);
+ Colour = Colour4.White;
+ RelativePositionAxes = Axes.Both;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ });
+ }
+
+ protected override void OnApply()
+ {
+ base.OnApply();
+
+ X = (float)((HitObject.StartTime - ParentHitObject!.HitObject.StartTime) / (ParentHitObject.HitObject.GetEndTime() - ParentHitObject.HitObject.StartTime));
+ Y = 0.5f;
+
+ LifetimeStart = ParentHitObject.LifetimeStart;
+ LifetimeEnd = ParentHitObject.LifetimeEnd;
+ }
+
+ protected override void CheckForResult(bool userTriggered, double timeOffset)
+ {
+ base.CheckForResult(userTriggered, timeOffset);
+ if (timeOffset >= 0)
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ }
+ }
+
#endregion
}
}
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..0439656aae 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/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index 5c69062e67..3f78dbfd96 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.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 System.Linq;
@@ -22,8 +20,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneSkinnableSound : OsuTestScene
{
- private TestSkinSourceContainer skinSource;
- private PausableSkinnableSound skinnableSound;
+ private TestSkinSourceContainer skinSource = null!;
+ private PausableSkinnableSound skinnableSound = null!;
+
+ private const string sample_lookup = "Gameplay/normal-sliderslide";
[SetUpSteps]
public void SetUpSteps()
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
};
// has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached.
- skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide")));
+ skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo(sample_lookup)));
});
}
@@ -99,10 +99,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample not playing", () => !skinnableSound.IsPlaying);
}
+ [Test]
+ public void TestSampleUpdatedBeforePlaybackWhenNotPresent()
+ {
+ AddStep("make sample non-present", () => skinnableSound.Hide());
+ AddUntilStep("ensure not present", () => skinnableSound.IsPresent, () => Is.False);
+
+ AddUntilStep("ensure sample loaded", () => skinnableSound.ChildrenOfType().Single().Name, () => Is.EqualTo(sample_lookup));
+
+ AddStep("change source", () =>
+ {
+ skinSource.OverridingSample = new SampleVirtual("new skin");
+ skinSource.TriggerSourceChanged();
+ });
+
+ AddStep("start sample", () => skinnableSound.Play());
+ AddUntilStep("sample updated", () => skinnableSound.ChildrenOfType().Single().Name, () => Is.EqualTo("new skin"));
+ }
+
[Test]
public void TestSkinChangeDoesntPlayOnPause()
{
- DrawableSample sample = null;
+ DrawableSample? sample = null;
AddStep("start sample", () =>
{
skinnableSound.Play();
@@ -118,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("retrieve and ensure current sample is different", () =>
{
- DrawableSample oldSample = sample;
+ DrawableSample? oldSample = sample;
sample = skinnableSound.ChildrenOfType().Single();
return sample != oldSample;
});
@@ -134,20 +152,29 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{
[Resolved]
- private ISkinSource source { get; set; }
+ private ISkinSource source { get; set; } = null!;
- public event Action SourceChanged;
+ public event Action? SourceChanged;
public Bindable SamplePlaybackDisabled { get; } = new Bindable();
+ public ISample? OverridingSample;
+
IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
- public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => source?.GetDrawableComponent(lookup);
- public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
- public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
- public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup);
- public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction);
- public IEnumerable AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty());
+ public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => source.GetDrawableComponent(lookup);
+ public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source.GetTexture(componentName, wrapModeS, wrapModeT);
+ public ISample? GetSample(ISampleInfo sampleInfo) => OverridingSample ?? source.GetSample(sampleInfo);
+
+ public IBindable? GetConfig(TLookup lookup)
+ where TLookup : notnull
+ where TValue : notnull
+ {
+ return source.GetConfig(lookup);
+ }
+
+ public ISkin? FindProvider(Func lookupFunction) => lookupFunction(this) ? this : source.FindProvider(lookupFunction);
+ public IEnumerable AllSources => new[] { this }.Concat(source.AllSources);
public void TriggerSourceChanged()
{
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/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
new file mode 100644
index 0000000000..603573058e
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Menu;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
+ {
+ ///
+ /// When entering the editor, a new beatmap is created as part of the asynchronous load process.
+ /// This test ensures that in the case of an early exit from the editor (ie. while it's still loading)
+ /// doesn't leave a dangling beatmap behind.
+ ///
+ /// This may not fail 100% due to timing, but has a pretty high chance of hitting a failure so works well enough
+ /// as a test.
+ ///
+ [Test]
+ public void TestCancelNavigationToEditor()
+ {
+ BeatmapSetInfo[] beatmapSets = null!;
+
+ AddStep("Fetch initial beatmaps", () => beatmapSets = allBeatmapSets());
+
+ AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault());
+
+ AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader()));
+ AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader);
+ AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit());
+
+ AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
+ AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets));
+
+ BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All().Where(x => !x.DeletePending).ToArray());
+ }
+ }
+}
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/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 5d13421195..a27c4ddad2 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -26,7 +27,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Online
{
- public partial class TestSceneBeatmapSetOverlay : OsuTestScene
+ public partial class TestSceneBeatmapSetOverlay : OsuManualInputManagerTestScene
{
private readonly TestBeatmapSetOverlay overlay;
@@ -281,6 +282,22 @@ namespace osu.Game.Tests.Visual.Online
AddAssert(@"type is correct", () => type == lookupType.ToString());
}
+ [Test]
+ public void TestBeatmapSetWithGuestDifficulty()
+ {
+ AddStep("show map", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty()));
+ AddStep("move mouse to host difficulty", () =>
+ {
+ InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(0));
+ });
+ AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().All(s => s.Text != "BanchoBot"));
+ AddStep("move mouse to guest difficulty", () =>
+ {
+ InputManager.MoveMouseTo(overlay.ChildrenOfType().ElementAt(1));
+ });
+ AddAssert("guest mapper information shown", () => overlay.ChildrenOfType().Single().ChildrenOfType().Any(s => s.Text == "BanchoBot"));
+ }
+
private APIBeatmapSet createManyDifficultiesBeatmapSet()
{
var set = getBeatmapSet();
@@ -320,6 +337,60 @@ namespace osu.Game.Tests.Visual.Online
return beatmapSet;
}
+ private APIBeatmapSet createBeatmapSetWithGuestDifficulty()
+ {
+ var set = getBeatmapSet();
+
+ var beatmaps = new List();
+
+ var guestUser = new APIUser
+ {
+ Username = @"BanchoBot",
+ Id = 3,
+ };
+
+ set.RelatedUsers = new[]
+ {
+ set.Author, guestUser
+ };
+
+ beatmaps.Add(new APIBeatmap
+ {
+ OnlineID = 1145,
+ DifficultyName = "Host Diff",
+ RulesetID = Ruleset.Value.OnlineID,
+ StarRating = 1.4,
+ OverallDifficulty = 3.5f,
+ AuthorID = set.AuthorID,
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
+ },
+ Status = BeatmapOnlineStatus.Graveyard
+ });
+
+ beatmaps.Add(new APIBeatmap
+ {
+ OnlineID = 1919,
+ DifficultyName = "Guest Diff",
+ RulesetID = Ruleset.Value.OnlineID,
+ StarRating = 8.1,
+ OverallDifficulty = 3.5f,
+ AuthorID = 3,
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
+ },
+ Status = BeatmapOnlineStatus.Graveyard
+ });
+
+ set.Beatmaps = beatmaps.ToArray();
+
+ return set;
+ }
+
private void downloadAssert(bool shown)
{
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown);
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/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 65ce0429fc..61a8322ee3 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -516,15 +516,20 @@ namespace osu.Game.Tests.Visual.SongSelect
{
sets.Clear();
- for (int i = 0; i < 20; i++)
+ for (int i = 0; i < 10; i++)
{
var set = TestResources.CreateTestBeatmapSetInfo(5);
- if (i >= 2 && i < 10)
+ // A total of 6 sets have date submitted (4 don't)
+ // A total of 5 sets have artist string (3 of which also have date submitted)
+
+ if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date
set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i);
- if (i < 5)
+ if (i < 5) // i = 0, 1, 2, 3, 4 have matching string
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
+ set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}");
+
sets.Add(set);
}
});
@@ -532,15 +537,26 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false));
- checkVisibleItemCount(diff: false, count: 8);
+ checkVisibleItemCount(diff: false, count: 10);
checkVisibleItemCount(diff: true, count: 5);
+
+ AddAssert("missing date are at end",
+ () => carousel.Items.OfType().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(4));
+ AddAssert("rest are at start", () => carousel.Items.OfType().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
+ () => Is.EqualTo(6));
+
AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria
{
Sort = SortMode.DateSubmitted,
SearchText = zzz_string
}, false));
- checkVisibleItemCount(diff: false, count: 3);
+ checkVisibleItemCount(diff: false, count: 5);
checkVisibleItemCount(diff: true, count: 5);
+
+ AddAssert("missing date are at end",
+ () => carousel.Items.OfType().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(2));
+ AddAssert("rest are at start", () => carousel.Items.OfType().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
+ () => Is.EqualTo(3));
}
[Test]
@@ -1129,7 +1145,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
// until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet.
AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () =>
- carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count);
+ carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible), () => Is.EqualTo(count));
}
private void checkSelectionIsCentered()
@@ -1190,8 +1206,11 @@ namespace osu.Game.Tests.Visual.SongSelect
{
get
{
- foreach (var item in Scroll.Children)
+ foreach (var item in Scroll.Children.OrderBy(c => c.Y))
{
+ if (item.Item?.Visible != true)
+ continue;
+
yield return item;
if (item is DrawableCarouselBeatmapSet set)
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/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index feab86d3ee..f094d40caa 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -1068,6 +1068,21 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("options disabled", () => !songSelect.ChildrenOfType().Single().Enabled.Value);
}
+ [Test]
+ public void TestTextBoxBeatmapDifficultyCount()
+ {
+ createSongSelect();
+
+ AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches");
+
+ addRulesetImportStep(0);
+
+ AddAssert("3 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "3 matches");
+ AddStep("delete all beatmaps", () => manager.Delete());
+ AddUntilStep("wait for no beatmap", () => Beatmap.IsDefault);
+ AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches");
+ }
+
private void waitForInitialSelection()
{
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
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.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 5ccaebd721..f99fe1d8d4 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -14,6 +14,7 @@ using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
@@ -67,6 +68,19 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}
});
+ r.Add(new ModPreset
+ {
+ Name = "Half Time 0.5x",
+ Description = "Very slow",
+ Ruleset = r.Find(OsuRuleset.SHORT_NAME),
+ Mods = new[]
+ {
+ new OsuModHalfTime
+ {
+ SpeedChange = { Value = 0.5 }
+ }
+ }
+ });
});
});
}
@@ -566,6 +580,28 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("5 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 5);
}
+ [Test]
+ public void TestModMultiplierUpdates()
+ {
+ createScreen();
+
+ AddStep("select mod preset with half time", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
+ InputManager.Click(MouseButton.Left);
+ });
+ AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.5));
+
+ // this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
+ // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
+ AddStep("force collection", GC.Collect);
+
+ AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
+ AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single()
+ .ChildrenOfType>().Single().TriggerClick());
+ AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7));
+ }
+
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
() => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded));
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/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index 4752a88199..4731a70753 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
- public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
+ public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public BeatmapImporter(Storage storage, RealmAccess realm)
: base(storage, realm)
@@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps
first.PerformRead(s =>
{
// Re-run processing even in this case. We might have outdated metadata.
- ProcessBeatmap?.Invoke((s, false));
+ ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
});
return first;
}
@@ -206,7 +206,7 @@ namespace osu.Game.Beatmaps
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{
base.PostImport(model, realm, parameters);
- ProcessBeatmap?.Invoke((model, parameters.Batch));
+ ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst);
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 3208598f56..63e878b80d 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -167,7 +167,7 @@ namespace osu.Game.Beatmaps
///
public double DistanceSpacing { get; set; } = 1.0;
- public int BeatDivisor { get; set; }
+ public int BeatDivisor { get; set; } = 4;
public int GridSize { get; set; }
diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
index eab66b9857..3aab9a24e1 100644
--- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
+++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
@@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
+using System.Collections.Generic;
using osu.Framework.Localisation;
namespace osu.Game.Beatmaps
@@ -29,10 +29,21 @@ namespace osu.Game.Beatmaps
return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim());
}
- public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[]
+ public static List GetSearchableTerms(this IBeatmapInfo beatmapInfo)
{
- beatmapInfo.DifficultyName
- }.Concat(beatmapInfo.Metadata.GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
+ var termsList = new List(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1);
+
+ addIfNotNull(beatmapInfo.DifficultyName);
+
+ BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList);
+ return termsList;
+
+ void addIfNotNull(string? s)
+ {
+ if (!string.IsNullOrEmpty(s))
+ termsList.Add(s);
+ }
+ }
private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]";
}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index ad56bbbc3a..ae62564b0d 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps
private readonly WorkingBeatmapCache workingBeatmapCache;
- public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
+ public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
public override bool PauseImports
{
@@ -72,7 +72,7 @@ namespace osu.Game.Beatmaps
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapImporter = CreateBeatmapImporter(storage, realm);
- beatmapImporter.ProcessBeatmap = args => ProcessBeatmap?.Invoke(args);
+ beatmapImporter.ProcessBeatmap = (beatmapSet, scope) => ProcessBeatmap?.Invoke(beatmapSet, scope);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
@@ -368,7 +368,7 @@ namespace osu.Game.Beatmaps
// user requested abort
return;
- var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
+ var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
if (video != null)
{
@@ -415,6 +415,13 @@ namespace osu.Game.Beatmaps
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
+ // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.
+ // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file,
+ // which influences the beatmap checksums.
+ beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
+ beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
+ beatmapInfo.ResetOnlineInfo();
+
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
@@ -438,9 +445,6 @@ namespace osu.Game.Beatmaps
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash();
- beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
- beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
-
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
updateHashAndMarkDirty(setInfo);
@@ -454,7 +458,9 @@ namespace osu.Game.Beatmaps
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
- ProcessBeatmap?.Invoke((liveBeatmapSet, false));
+ // do not look up metadata.
+ // this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
+ ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);
});
}
@@ -542,4 +548,11 @@ namespace osu.Game.Beatmaps
public override string HumanisedModelName => "beatmap";
}
+
+ ///