diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
deleted file mode 100644
index 7026179259..0000000000
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ /dev/null
@@ -1,30 +0,0 @@
----
-name: Bug Report
-about: Report a bug or crash to desktop
----
-
-
-
-
-**Describe the bug:**
-
-**Screenshots or videos showing encountered issue:**
-
-**osu!lazer version:**
-
-**Logs:**
-
-
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index c62231e8e0..47a6a4c3d3 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,12 +1,12 @@
blank_issues_enabled: false
contact_links:
- - name: Suggestions or feature request
- url: https://github.com/ppy/osu/discussions/categories/ideas
- about: Got something you think should change or be added? Search for or start a new discussion!
- name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
+ - name: Suggestions or feature request
+ url: https://github.com/ppy/osu/discussions/categories/ideas
+ about: Got something you think should change or be added? Search for or start a new discussion!
- name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues
- about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.
+ about: For osu!(stable) - ie. the current "live" game version, check out the dedicated repository. Note that this is for serious bug reports only, not tech support.
diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml
new file mode 100644
index 0000000000..bc2626d3d6
--- /dev/null
+++ b/.github/workflows/diffcalc.yml
@@ -0,0 +1,205 @@
+# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
+# Usage:
+# !pp check 0 | Runs only the osu! ruleset.
+# !pp check 0 2 | Runs only the osu! and catch rulesets.
+#
+
+name: Difficulty Calculation
+on:
+ issue_comment:
+ types: [ created ]
+
+env:
+ CONCURRENCY: 4
+ ALLOW_DOWNLOAD: 1
+ SAVE_DOWNLOADED: 1
+ SKIP_INSERT_ATTRIBUTES: 1
+
+jobs:
+ metadata:
+ name: Check for requests
+ runs-on: self-hosted
+ if: github.event.issue.pull_request && contains(github.event.comment.body, '!pp check') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
+ outputs:
+ matrix: ${{ steps.generate-matrix.outputs.matrix }}
+ continue: ${{ steps.generate-matrix.outputs.continue }}
+ steps:
+ - name: Construct build matrix
+ id: generate-matrix
+ run: |
+ if [[ "${{ github.event.comment.body }}" =~ "osu" ]] ; then
+ MATRIX_PROJECTS_JSON+='{ "name": "osu", "id": 0 },'
+ fi
+ if [[ "${{ github.event.comment.body }}" =~ "taiko" ]] ; then
+ MATRIX_PROJECTS_JSON+='{ "name": "taiko", "id": 1 },'
+ fi
+ if [[ "${{ github.event.comment.body }}" =~ "catch" ]] ; then
+ MATRIX_PROJECTS_JSON+='{ "name": "catch", "id": 2 },'
+ fi
+ if [[ "${{ github.event.comment.body }}" =~ "mania" ]] ; then
+ MATRIX_PROJECTS_JSON+='{ "name": "mania", "id": 3 },'
+ fi
+
+ if [[ "${MATRIX_PROJECTS_JSON}" != "" ]]; then
+ MATRIX_JSON="{ \"ruleset\": [ ${MATRIX_PROJECTS_JSON} ] }"
+ echo "${MATRIX_JSON}"
+ CONTINUE="yes"
+ else
+ CONTINUE="no"
+ fi
+
+ echo "::set-output name=continue::${CONTINUE}"
+ echo "::set-output name=matrix::${MATRIX_JSON}"
+ diffcalc:
+ name: Run
+ runs-on: self-hosted
+ if: needs.metadata.outputs.continue == 'yes'
+ needs: metadata
+ strategy:
+ matrix: ${{ fromJson(needs.metadata.outputs.matrix) }}
+ steps:
+ - name: Verify MySQL connection from host
+ run: |
+ mysql -e "SHOW DATABASES"
+
+ - name: Drop previous databases
+ run: |
+ for db in osu_master osu_pr
+ do
+ mysql -e "DROP DATABASE IF EXISTS $db"
+ done
+
+ - name: Create directory structure
+ run: |
+ mkdir -p $GITHUB_WORKSPACE/master/
+ mkdir -p $GITHUB_WORKSPACE/pr/
+
+ - name: Get upstream branch # https://akaimo.hatenablog.jp/entry/2020/05/16/101251
+ id: upstreambranch
+ 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')"
+
+ # Checkout osu
+ - name: Checkout osu (master)
+ uses: actions/checkout@v2
+ with:
+ path: 'master/osu'
+ - name: Checkout osu (pr)
+ uses: actions/checkout@v2
+ with:
+ path: 'pr/osu'
+ repository: ${{ steps.upstreambranch.outputs.repo }}
+ ref: ${{ steps.upstreambranch.outputs.branchname }}
+
+ - name: Checkout osu-difficulty-calculator (master)
+ uses: actions/checkout@v2
+ with:
+ repository: ppy/osu-difficulty-calculator
+ path: 'master/osu-difficulty-calculator'
+ - name: Checkout osu-difficulty-calculator (pr)
+ uses: actions/checkout@v2
+ with:
+ repository: ppy/osu-difficulty-calculator
+ path: 'pr/osu-difficulty-calculator'
+
+ - name: Install .NET 5.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "5.0.x"
+
+ # Sanity checks to make sure diffcalc is not run when incompatible.
+ - name: Build diffcalc (master)
+ run: |
+ cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
+ ./UseLocalOsu.sh
+ dotnet build
+ - name: Build diffcalc (pr)
+ run: |
+ cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
+ ./UseLocalOsu.sh
+ dotnet build
+
+ - name: Download + import data
+ run: |
+ PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top_1000 | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
+ BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
+
+ # Set env variable for further steps.
+ echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
+
+ cd $GITHUB_WORKSPACE
+
+ echo "Downloading database dump $PERFORMANCE_DATA_NAME.."
+ wget -q -nc https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
+ echo "Extracting.."
+ tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
+
+ echo "Downloading beatmap dump $BEATMAPS_DATA_NAME.."
+ wget -q -nc https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
+ echo "Extracting.."
+ tar -xf $BEATMAPS_DATA_NAME.tar.bz2
+
+ cd $PERFORMANCE_DATA_NAME
+
+ for db in osu_master osu_pr
+ do
+ echo "Setting up database $db.."
+
+ mysql -e "CREATE DATABASE $db"
+
+ echo "Importing beatmaps.."
+ cat osu_beatmaps.sql | mysql $db
+ echo "Importing beatmapsets.."
+ cat osu_beatmapsets.sql | mysql $db
+
+ echo "Creating table structure.."
+ mysql $db -e 'CREATE TABLE `osu_beatmap_difficulty` (
+ `beatmap_id` int unsigned NOT NULL,
+ `mode` tinyint NOT NULL DEFAULT 0,
+ `mods` int unsigned NOT NULL,
+ `diff_unified` float NOT NULL,
+ `last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`beatmap_id`,`mode`,`mods`),
+ KEY `diff_sort` (`mode`,`mods`,`diff_unified`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;'
+ done
+
+ - name: Run diffcalc (master)
+ env:
+ DB_NAME: osu_master
+ run: |
+ cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
+ dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
+ - name: Run diffcalc (pr)
+ env:
+ DB_NAME: osu_pr
+ run: |
+ cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
+ dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
+
+ - name: Print diffs
+ run: |
+ mysql -e "
+ SELECT
+ m.beatmap_id,
+ m.mods,
+ b.filename,
+ m.diff_unified as 'sr_master',
+ p.diff_unified as 'sr_pr',
+ (p.diff_unified - m.diff_unified) as 'diff'
+ FROM osu_master.osu_beatmap_difficulty m
+ JOIN osu_pr.osu_beatmap_difficulty p
+ ON m.beatmap_id = p.beatmap_id
+ AND m.mode = p.mode
+ AND m.mods = p.mods
+ JOIN osu_pr.osu_beatmaps b
+ ON b.beatmap_id = p.beatmap_id
+ WHERE abs(m.diff_unified - p.diff_unified) > 0.1
+ ORDER BY abs(m.diff_unified - p.diff_unified)
+ DESC
+ LIMIT 10000;"
+
+ # Todo: Run ppcalc
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
index 8fa7608b8e..498a710df9 100644
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
+++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml
@@ -1,8 +1,8 @@
-
-
-
+
+
+
@@ -14,7 +14,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/.idea.osu/.idea/indexLayout.xml b/.idea/.idea.osu/.idea/indexLayout.xml
index 27ba142e96..7b08163ceb 100644
--- a/.idea/.idea.osu/.idea/indexLayout.xml
+++ b/.idea/.idea.osu/.idea/indexLayout.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/README.md b/README.md
index 8f922f74a7..786ce2589d 100644
--- a/README.md
+++ b/README.md
@@ -31,12 +31,11 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:**
-| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
+| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
-- When running on Windows 7 or 8.1, *[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net50&pivots=os-windows#dependencies)** may be required to correctly run .NET 5 applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
## Developing a custom ruleset
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
index 9c512a01ea..536fdfc6df 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/TestSceneOsuGame.cs
@@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests
[BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase)
{
- OsuGame game = new OsuGame();
- game.SetHost(host);
-
Children = new Drawable[]
{
new Box
@@ -25,8 +22,9 @@ namespace osu.Game.Rulesets.EmptyFreeform.Tests
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
- game
};
+
+ AddGame(new OsuGame());
}
}
}
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
index 270d906b01..3cdf44e6f1 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
@@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Pippidon.Tests
[BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase)
{
- OsuGame game = new OsuGame();
- game.SetHost(host);
-
Children = new Drawable[]
{
new Box
@@ -25,8 +22,9 @@ namespace osu.Game.Rulesets.Pippidon.Tests
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
- game
};
+
+ AddGame(new OsuGame());
}
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
index aed6abb6bf..4d3f5086d9 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/TestSceneOsuGame.cs
@@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests
[BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase)
{
- OsuGame game = new OsuGame();
- game.SetHost(host);
-
Children = new Drawable[]
{
new Box
@@ -25,8 +22,9 @@ namespace osu.Game.Rulesets.EmptyScrolling.Tests
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
- game
};
+
+ AddGame(new OsuGame());
}
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
index 270d906b01..3cdf44e6f1 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/TestSceneOsuGame.cs
@@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Pippidon.Tests
[BackgroundDependencyLoader]
private void load(GameHost host, OsuGameBase gameBase)
{
- OsuGame game = new OsuGame();
- game.SetHost(host);
-
Children = new Drawable[]
{
new Box
@@ -25,8 +22,9 @@ namespace osu.Game.Rulesets.Pippidon.Tests
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
- game
};
+
+ AddGame(new OsuGame());
}
}
}
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs
index dd0a20f1b4..98dba622d0 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/UI/PippidonCharacter.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osuTK;
@@ -61,9 +62,9 @@ namespace osu.Game.Rulesets.Pippidon.UI
}
}
- public bool OnPressed(PippidonAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- switch (action)
+ switch (e.Action)
{
case PippidonAction.MoveUp:
changeLane(-1);
@@ -78,7 +79,7 @@ namespace osu.Game.Rulesets.Pippidon.UI
}
}
- public void OnReleased(PippidonAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Android.props b/osu.Android.props
index 8a9bf1b9cd..27df6cf296 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
-
-
+
+
-
+
diff --git a/osu.Game.Benchmarks/BenchmarkMod.cs b/osu.Game.Benchmarks/BenchmarkMod.cs
index 050ddf36d5..c5375e9f09 100644
--- a/osu.Game.Benchmarks/BenchmarkMod.cs
+++ b/osu.Game.Benchmarks/BenchmarkMod.cs
@@ -14,9 +14,9 @@ namespace osu.Game.Benchmarks
[Params(1, 10, 100)]
public int Times { get; set; }
- [GlobalSetup]
- public void GlobalSetup()
+ public override void SetUp()
{
+ base.SetUp();
mod = new OsuModDoubleTime();
}
diff --git a/osu.Game.Benchmarks/BenchmarkRuleset.cs b/osu.Game.Benchmarks/BenchmarkRuleset.cs
new file mode 100644
index 0000000000..2835ec9499
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkRuleset.cs
@@ -0,0 +1,62 @@
+// 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 BenchmarkDotNet.Engines;
+using osu.Game.Online.API;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkRuleset : BenchmarkTest
+ {
+ private OsuRuleset ruleset;
+ private APIMod apiModDoubleTime;
+ private APIMod apiModDifficultyAdjust;
+
+ public override void SetUp()
+ {
+ base.SetUp();
+ ruleset = new OsuRuleset();
+ apiModDoubleTime = new APIMod { Acronym = "DT" };
+ apiModDifficultyAdjust = new APIMod { Acronym = "DA" };
+ }
+
+ [Benchmark]
+ public void BenchmarkToModDoubleTime()
+ {
+ apiModDoubleTime.ToMod(ruleset);
+ }
+
+ [Benchmark]
+ public void BenchmarkToModDifficultyAdjust()
+ {
+ apiModDifficultyAdjust.ToMod(ruleset);
+ }
+
+ [Benchmark]
+ public void BenchmarkGetAllMods()
+ {
+ ruleset.CreateAllMods().Consume(new Consumer());
+ }
+
+ [Benchmark]
+ public void BenchmarkGetAllModsForReference()
+ {
+ ruleset.AllMods.Consume(new Consumer());
+ }
+
+ [Benchmark]
+ public void BenchmarkGetForAcronym()
+ {
+ ruleset.CreateModFromAcronym("DT");
+ }
+
+ [Benchmark]
+ public void BenchmarkGetForType()
+ {
+ ruleset.CreateMod();
+ }
+ }
+}
diff --git a/osu.Game.Benchmarks/Program.cs b/osu.Game.Benchmarks/Program.cs
index c55075fea6..439ced53ab 100644
--- a/osu.Game.Benchmarks/Program.cs
+++ b/osu.Game.Benchmarks/Program.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 BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
namespace osu.Game.Benchmarks
@@ -11,7 +12,7 @@ namespace osu.Game.Benchmarks
{
BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly)
- .Run(args);
+ .Run(args, DefaultConfig.Instance.WithOption(ConfigOptions.DisableOptimizationsValidator, true));
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 9feaa55051..82d76252d2 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
foreach (var hitObject in beatmap.HitObjects
- .SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects : new[] { obj })
+ .SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj })
.Cast()
.OrderBy(x => x.StartTime))
{
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 73b60f51a4..d0a94767d1 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Mods
}
// disable keyboard controls
- public bool OnPressed(CatchAction action) => true;
+ public bool OnPressed(KeyBindingPressEvent e) => true;
- public void OnReleased(CatchAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index b30c3d82a4..604e878782 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Replays;
@@ -144,9 +145,9 @@ namespace osu.Game.Rulesets.Catch.UI
Catcher.VisualDirection = Direction.Left;
}
- public bool OnPressed(CatchAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- switch (action)
+ switch (e.Action)
{
case CatchAction.MoveLeft:
currentDirection--;
@@ -164,9 +165,9 @@ namespace osu.Game.Rulesets.Catch.UI
return false;
}
- public void OnReleased(CatchAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
- switch (action)
+ switch (e.Action)
{
case CatchAction.MoveLeft:
currentDirection++;
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1-0@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1-0@2x.png
new file mode 100644
index 0000000000..2db5d76e78
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1-0@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1-1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1-1@2x.png
new file mode 100644
index 0000000000..6e7aded39f
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1-1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1H-0@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1H-0@2x.png
new file mode 100644
index 0000000000..f11fb4f853
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1H-0@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1H-1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1H-1@2x.png
new file mode 100644
index 0000000000..4eac5f6f2a
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note1H-1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2-0@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2-0@2x.png
new file mode 100644
index 0000000000..456cee5382
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2-0@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2-1@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2-1@2x.png
new file mode 100644
index 0000000000..71a09cb4bb
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2-1@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2H@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2H@2x.png
new file mode 100644
index 0000000000..e6da7a1055
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-note2H@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-noteS@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-noteS@2x.png
new file mode 100644
index 0000000000..c9bc23e8d9
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-noteS@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-noteSH@2x.png b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-noteSH@2x.png
new file mode 100644
index 0000000000..c9bc23e8d9
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/metrics-skin/mania-noteSH@2x.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index b7d7af6b8c..68cf3b67df 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
c.Add(CreateHitObject().With(h =>
{
- h.HitObject.StartTime = START_TIME;
+ h.HitObject.StartTime = Time.Current + 5000;
h.AccentColour.Value = Color4.Orange;
}));
})
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
c.Add(CreateHitObject().With(h =>
{
- h.HitObject.StartTime = START_TIME;
+ h.HitObject.StartTime = Time.Current + 5000;
h.AccentColour.Value = Color4.Orange;
}));
})
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
index 1d84a2dfcb..ddfd057cd8 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
@@ -19,8 +19,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
///
public abstract class ManiaSkinnableTestScene : SkinnableTestScene
{
- protected const double START_TIME = 1000000000;
-
[Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
@@ -55,27 +53,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
public readonly Bindable Direction = new Bindable();
IBindable IScrollingInfo.Direction => Direction;
- IBindable IScrollingInfo.TimeRange { get; } = new Bindable(1000);
- IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ZeroScrollAlgorithm();
- }
-
- private class ZeroScrollAlgorithm : IScrollAlgorithm
- {
- public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
- => double.MinValue;
-
- public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
- => scrollLength;
-
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
- => (float)((time - START_TIME) / timeRange) * scrollLength;
-
- public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
- => 0;
-
- public void Reset()
- {
- }
+ IBindable IScrollingInfo.TimeRange { get; } = new Bindable(5000);
+ IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm();
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
index 4a6c59e297..92c95b8fde 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("Hold key", () =>
{
clock.CurrentTime = 0;
- note.OnPressed(ManiaAction.Key1);
+ note.OnPressed(new KeyBindingPressEvent(GetContainingInputManager().CurrentState, ManiaAction.Key1));
});
AddStep("progress time", () => clock.CurrentTime = 500);
AddAssert("head is visible", () => note.Head.Alpha == 1);
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
index e14ad92842..449a6ff23d 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
@@ -13,6 +14,10 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Framework.Bindables;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Tests
{
@@ -22,14 +27,65 @@ namespace osu.Game.Rulesets.Mania.Tests
[Resolved]
private RulesetConfigCache configCache { get; set; }
- private readonly Bindable configTimingBasedNoteColouring = new Bindable();
+ private Bindable configTimingBasedNoteColouring;
- protected override void LoadComplete()
+ private ManualClock clock;
+ private DrawableManiaRuleset drawableRuleset;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("setup hierarchy", () => Child = new Container
+ {
+ Clock = new FramedClock(clock = new ManualClock()),
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new[]
+ {
+ drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
+ }
+ });
+ AddStep("retrieve config bindable", () =>
+ {
+ var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
+ configTimingBasedNoteColouring = config.GetBindable(ManiaRulesetSetting.TimingBasedNoteColouring);
+ });
+ }
+
+ [Test]
+ public void TestSimple()
+ {
+ AddStep("enable", () => configTimingBasedNoteColouring.Value = true);
+ AddStep("disable", () => configTimingBasedNoteColouring.Value = false);
+ }
+
+ [Test]
+ public void TestToggleOffScreen()
+ {
+ AddStep("enable", () => configTimingBasedNoteColouring.Value = true);
+
+ seekTo(10000);
+ AddStep("disable", () => configTimingBasedNoteColouring.Value = false);
+ seekTo(0);
+ AddAssert("all notes not coloured", () => this.ChildrenOfType().All(note => note.Colour == Colour4.White));
+
+ seekTo(10000);
+ AddStep("enable again", () => configTimingBasedNoteColouring.Value = true);
+ seekTo(0);
+ AddAssert("some notes coloured", () => this.ChildrenOfType().Any(note => note.Colour != Colour4.White));
+ }
+
+ private void seekTo(double time)
+ {
+ AddStep($"seek to {time}", () => clock.CurrentTime = time);
+ AddUntilStep("wait for seek", () => Precision.AlmostEquals(drawableRuleset.FrameStableClock.CurrentTime, time, 1));
+ }
+
+ private ManiaBeatmap createTestBeatmap()
{
const double beat_length = 500;
- var ruleset = new ManiaRuleset();
-
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
{
HitObjects =
@@ -45,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new Note { StartTime = beat_length }
},
ControlPointInfo = new ControlPointInfo(),
- BeatmapInfo = { Ruleset = ruleset.RulesetInfo },
+ BeatmapInfo = { Ruleset = Ruleset.Value },
};
foreach (var note in beatmap.HitObjects)
@@ -57,24 +113,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
BeatLength = beat_length
});
-
- Child = new Container
- {
- Clock = new FramedClock(new ManualClock()),
- RelativeSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new[]
- {
- ruleset.CreateDrawableRulesetWith(beatmap)
- }
- };
-
- var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
- config.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
-
- AddStep("Enable", () => configTimingBasedNoteColouring.Value = true);
- AddStep("Disable", () => configTimingBasedNoteColouring.Value = false);
+ return beatmap;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
index fb58d805a9..e643b82271 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs
@@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
lowerBound ??= RandomStart;
upperBound ??= TotalColumns;
- nextColumn ??= (_ => GetRandomColumn(lowerBound, upperBound));
+ nextColumn ??= _ => GetRandomColumn(lowerBound, upperBound);
// Check for the initial column
if (isValid(initialColumn))
@@ -176,7 +176,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return initialColumn;
- bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column));
+ bool isValid(int column)
+ {
+ if (validation?.Invoke(column) == false)
+ return false;
+
+ foreach (var p in patterns)
+ {
+ if (p.ColumnHasObject(column))
+ return false;
+ }
+
+ return true;
+ }
}
///
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs
index f095a0ffce..828f2ec393 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs
@@ -12,46 +12,68 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
///
internal class Pattern
{
- private readonly List hitObjects = new List();
+ private List hitObjects;
+ private HashSet containedColumns;
///
/// All the hit objects contained in this pattern.
///
- public IEnumerable HitObjects => hitObjects;
+ public IEnumerable HitObjects => hitObjects ?? Enumerable.Empty();
///
/// Check whether a column of this patterns contains a hit object.
///
/// The column index.
/// Whether the column with index contains a hit object.
- public bool ColumnHasObject(int column) => hitObjects.Exists(h => h.Column == column);
+ public bool ColumnHasObject(int column) => containedColumns?.Contains(column) == true;
///
/// Amount of columns taken up by hit objects in this pattern.
///
- public int ColumnWithObjects => HitObjects.GroupBy(h => h.Column).Count();
+ public int ColumnWithObjects => containedColumns?.Count ?? 0;
///
/// Adds a hit object to this pattern.
///
/// The hit object to add.
- public void Add(ManiaHitObject hitObject) => hitObjects.Add(hitObject);
+ public void Add(ManiaHitObject hitObject)
+ {
+ prepareStorage();
+
+ hitObjects.Add(hitObject);
+ containedColumns.Add(hitObject.Column);
+ }
///
/// Copies hit object from another pattern to this one.
///
/// The other pattern.
- public void Add(Pattern other) => hitObjects.AddRange(other.HitObjects);
+ public void Add(Pattern other)
+ {
+ prepareStorage();
+
+ if (other.hitObjects != null)
+ {
+ hitObjects.AddRange(other.hitObjects);
+
+ foreach (var h in other.hitObjects)
+ containedColumns.Add(h.Column);
+ }
+ }
///
/// Clears this pattern, removing all hit objects.
///
- public void Clear() => hitObjects.Clear();
+ public void Clear()
+ {
+ hitObjects?.Clear();
+ containedColumns?.Clear();
+ }
- ///
- /// Removes a hit object from this pattern.
- ///
- /// The hit object to remove.
- public bool Remove(ManiaHitObject hitObject) => hitObjects.Remove(hitObject);
+ private void prepareStorage()
+ {
+ hitObjects ??= new List();
+ containedColumns ??= new HashSet();
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 2923a2af2f..4e9781f336 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -253,12 +254,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
HoldBrokenTime = Time.Current;
}
- public bool OnPressed(ManiaAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
if (AllJudged)
return false;
- if (action != Action.Value)
+ if (e.Action != Action.Value)
return false;
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
@@ -288,12 +289,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
isHitting.Value = true;
}
- public void OnReleased(ManiaAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
if (AllJudged)
return;
- if (action != Action.Value)
+ if (e.Action != Action.Value)
return;
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index 8458345998..6722ad8ab8 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
@@ -43,9 +44,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// it will be hidden along with its parenting hold note when required.
}
- public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
+ public override bool OnPressed(KeyBindingPressEvent e) => false; // Handled by the hold note
- public override void OnReleased(ManiaAction action)
+ public override void OnReleased(KeyBindingReleaseEvent e)
{
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index 18aa3f66d4..803685363c 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
@@ -68,9 +69,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
});
}
- public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
+ public override bool OnPressed(KeyBindingPressEvent e) => false; // Handled by the hold note
- public override void OnReleased(ManiaAction action)
+ public override void OnReleased(KeyBindingReleaseEvent e)
{
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
index 33d872dfb6..51727908c9 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Configuration;
@@ -66,6 +67,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true);
}
+ protected override void OnApply()
+ {
+ base.OnApply();
+ updateSnapColour();
+ }
+
protected override void OnDirectionChanged(ValueChangedEvent e)
{
base.OnDirectionChanged(e);
@@ -91,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r => r.Type = result);
}
- public virtual bool OnPressed(ManiaAction action)
+ public virtual bool OnPressed(KeyBindingPressEvent e)
{
- if (action != Action.Value)
+ if (e.Action != Action.Value)
return false;
if (CheckHittable?.Invoke(this, Time.Current) == false)
@@ -102,7 +109,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
return UpdateResult(true);
}
- public virtual void OnReleased(ManiaAction action)
+ public virtual void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs
index 661e7f66f4..54ddcbd5fe 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
@@ -76,9 +77,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}
}
- public bool OnPressed(ManiaAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- if (action == Column.Action.Value)
+ if (e.Action == Column.Action.Value)
{
light.FadeIn();
light.ScaleTo(Vector2.One);
@@ -87,12 +88,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return false;
}
- public void OnReleased(ManiaAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
// Todo: Should be 400 * 100 / CurrentBPM
const double animation_length = 250;
- if (action == Column.Action.Value)
+ if (e.Action == Column.Action.Value)
{
light.FadeTo(0, animation_length);
light.ScaleTo(new Vector2(1, 0), animation_length);
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs
index 21e5bdd5d6..1e75533442 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs
@@ -1,18 +1,18 @@
// 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.Textures;
+using osu.Framework.Graphics;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class LegacyHoldNoteHeadPiece : LegacyNotePiece
{
- protected override Texture GetTexture(ISkinSource skin)
+ protected override Drawable GetAnimation(ISkinSource skin)
{
// TODO: Should fallback to the head from default legacy skin instead of note.
- return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
- ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
+ return GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
+ ?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs
index 232b47ae27..e6d4291d79 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
-using osu.Framework.Graphics.Textures;
+using osu.Framework.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -18,12 +18,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
: new ValueChangedEvent(ScrollingDirection.Up, ScrollingDirection.Up));
}
- protected override Texture GetTexture(ISkinSource skin)
+ protected override Drawable GetAnimation(ISkinSource skin)
{
// TODO: Should fallback to the head from default legacy skin instead of note.
- return GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteTailImage)
- ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
- ?? GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
+ return GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteTailImage)
+ ?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
+ ?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
index 10319a7d4d..9c339345c4 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -86,9 +87,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}
}
- public bool OnPressed(ManiaAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- if (action == column.Action.Value)
+ if (e.Action == column.Action.Value)
{
upSprite.FadeTo(0);
downSprite.FadeTo(1);
@@ -97,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return false;
}
- public void OnReleased(ManiaAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
- if (action == column.Action.Value)
+ if (e.Action == column.Action.Value)
{
upSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(1);
downSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(0);
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs
index 31279796ce..321a87f8b1 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs
@@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
@@ -19,7 +21,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private readonly IBindable direction = new Bindable();
private Container directionContainer;
- private Sprite noteSprite;
+
+ [CanBeNull]
+ private Drawable noteAnimation;
private float? minimumColumnWidth;
@@ -39,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Child = noteSprite = new Sprite { Texture = GetTexture(skin) }
+ Child = noteAnimation = GetAnimation(skin) ?? Empty()
};
direction.BindTo(scrollingInfo.Direction);
@@ -50,12 +54,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
base.Update();
- if (noteSprite.Texture != null)
+ Texture texture = null;
+
+ if (noteAnimation is Sprite sprite)
+ texture = sprite.Texture;
+ else if (noteAnimation is TextureAnimation textureAnimation && textureAnimation.FrameCount > 0)
+ texture = textureAnimation.CurrentFrame;
+
+ if (texture != null)
{
// The height is scaled to the minimum column width, if provided.
float minimumWidth = minimumColumnWidth ?? DrawWidth;
-
- noteSprite.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), noteSprite.Texture.DisplayWidth);
+ noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), texture.DisplayWidth);
}
}
@@ -73,9 +83,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}
}
- protected virtual Texture GetTexture(ISkinSource skin) => GetTextureFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
+ [CanBeNull]
+ protected virtual Drawable GetAnimation(ISkinSource skin) => GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
- protected Texture GetTextureFromLookup(ISkin skin, LegacyManiaSkinConfigurationLookups lookup)
+ [CanBeNull]
+ protected Drawable GetAnimationFromLookup(ISkin skin, LegacyManiaSkinConfigurationLookups lookup)
{
string suffix = string.Empty;
@@ -93,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
string noteImage = GetColumnSkinConfig(skin, lookup)?.Value
?? $"mania-note{FallbackColumnIndex}{suffix}";
- return skin.GetTexture(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge);
+ return skin.GetAnimation(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index f5e30efd91..9d060944cd 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling;
@@ -122,16 +123,16 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
- public bool OnPressed(ManiaAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- if (action != Action.Value)
+ if (e.Action != Action.Value)
return false;
sampleTriggerSource.Play();
return true;
}
- public void OnReleased(ManiaAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
index 75cc351310..77ddc6fbbf 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK.Graphics;
@@ -91,16 +92,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components
direction.Value == ScrollingDirection.Up ? dimPoint : brightPoint);
}
- public bool OnPressed(ManiaAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- if (action == this.action.Value)
+ if (e.Action == action.Value)
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
return false;
}
- public void OnReleased(ManiaAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
- if (action == this.action.Value)
+ if (e.Action == action.Value)
backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
index 4b4bc157d5..807f6a77d9 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK.Graphics;
@@ -74,16 +75,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}
}
- public bool OnPressed(ManiaAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- if (action == column.Action.Value)
+ if (e.Action == column.Action.Value)
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
return false;
}
- public void OnReleased(ManiaAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
- if (action == column.Action.Value)
+ if (e.Action == column.Action.Value)
backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
index 47cb9bd45a..267ed1f5f4 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
@@ -101,16 +102,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}
}
- public bool OnPressed(ManiaAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- if (action == column.Action.Value)
+ if (e.Action == column.Action.Value)
keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint);
return false;
}
- public void OnReleased(ManiaAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
- if (action == column.Action.Value)
+ if (e.Action == column.Action.Value)
keyIcon.ScaleTo(1f, 125, Easing.OutQuint);
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
new file mode 100644
index 0000000000..47b2d3a098
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
@@ -0,0 +1,78 @@
+// 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.Testing;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu.Edit;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ public class TestSceneOsuEditorGrids : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ [Test]
+ public void TestGridExclusivity()
+ {
+ AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
+ AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
+ AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
+ rectangularGridActive(false);
+
+ AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
+ AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
+ rectangularGridActive(true);
+
+ AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
+ AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
+ AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
+ rectangularGridActive(false);
+ }
+
+ private void rectangularGridActive(bool active)
+ {
+ AddStep("choose placement tool", () => InputManager.Key(Key.Number2));
+ AddStep("move cursor to (1, 1)", () =>
+ {
+ var composer = Editor.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1)));
+ });
+
+ if (active)
+ AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(0, 0)));
+ else
+ AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(1, 1)));
+
+ AddStep("choose selection tool", () => InputManager.Key(Key.Number1));
+ }
+
+ [Test]
+ public void TestGridSizeToggling()
+ {
+ AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
+ AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any());
+ gridSizeIs(4);
+
+ nextGridSizeIs(8);
+ nextGridSizeIs(16);
+ nextGridSizeIs(32);
+ nextGridSizeIs(4);
+ }
+
+ private void nextGridSizeIs(int size)
+ {
+ AddStep("toggle to next grid size", () => InputManager.Key(Key.G));
+ gridSizeIs(size);
+ }
+
+ private void gridSizeIs(int size)
+ => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing == new Vector2(size)
+ && EditorBeatmap.BeatmapInfo.GridSize == size);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 8d8387378e..19881b5c33 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.7568168283591499d, "diffcalc-test")]
- [TestCase(1.0348244046058293d, "zero-length-sliders")]
+ [TestCase(6.6634445062299665d, "diffcalc-test")]
+ [TestCase(1.0414203870195022d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(8.4783236764532557d, "diffcalc-test")]
- [TestCase(1.2708532136987165d, "zero-length-sliders")]
+ [TestCase(8.3858089051603368d, "diffcalc-test")]
+ [TestCase(1.2723279173428435d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
new file mode 100644
index 0000000000..bd39dead34
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
@@ -0,0 +1,175 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Legacy;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneCursorParticles : TestSceneOsuPlayer
+ {
+ protected override bool Autoplay => autoplay;
+ protected override bool HasCustomSteps => true;
+
+ private bool autoplay;
+ private IBeatmap currentBeatmap;
+
+ [Resolved]
+ private SkinManager skinManager { get; set; }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentBeatmap ?? base.CreateBeatmap(ruleset);
+
+ [Test]
+ public void TestLegacyBreakParticles()
+ {
+ LegacyCursorParticles cursorParticles = null;
+
+ createLegacyTest(false, () => new Beatmap
+ {
+ Breaks =
+ {
+ new BreakPeriod(8500, 10000),
+ },
+ HitObjects =
+ {
+ new HitCircle
+ {
+ StartTime = 8000,
+ Position = OsuPlayfield.BASE_SIZE / 2,
+ },
+ new HitCircle
+ {
+ StartTime = 11000,
+ Position = OsuPlayfield.BASE_SIZE / 2,
+ },
+ }
+ });
+
+ AddUntilStep("fetch cursor particles", () =>
+ {
+ cursorParticles = this.ChildrenOfType().SingleOrDefault();
+ return cursorParticles != null;
+ });
+
+ AddStep("move mouse to centre", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
+
+ AddAssert("particles are being spawned", () => cursorParticles.Active);
+
+ AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left));
+ AddWaitStep("wait a bit", 5);
+ AddStep("press right mouse button", () => InputManager.PressButton(MouseButton.Right));
+ AddWaitStep("wait a bit", 5);
+ AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
+ AddWaitStep("wait a bit", 5);
+ AddStep("release right mouse button", () => InputManager.ReleaseButton(MouseButton.Right));
+
+ AddUntilStep("wait for beatmap start", () => !Player.IsBreakTime.Value);
+ AddAssert("particle spawning stopped", () => !cursorParticles.Active);
+
+ AddUntilStep("wait for break", () => Player.IsBreakTime.Value);
+ AddAssert("particles are being spawned", () => cursorParticles.Active);
+
+ AddUntilStep("wait for break end", () => !Player.IsBreakTime.Value);
+ AddAssert("particle spawning stopped", () => !cursorParticles.Active);
+ }
+
+ [Test]
+ public void TestLegacyKiaiParticles()
+ {
+ LegacyCursorParticles cursorParticles = null;
+ DrawableSpinner spinner = null;
+ DrawableSlider slider = null;
+
+ createLegacyTest(true, () =>
+ {
+ var controlPointInfo = new ControlPointInfo();
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+ controlPointInfo.Add(5000, new EffectControlPoint { KiaiMode = false });
+
+ return new Beatmap
+ {
+ ControlPointInfo = controlPointInfo,
+ HitObjects =
+ {
+ new Spinner
+ {
+ StartTime = 0,
+ Duration = 1000,
+ Position = OsuPlayfield.BASE_SIZE / 2,
+ },
+ new Slider
+ {
+ StartTime = 2500,
+ RepeatCount = 0,
+ Position = OsuPlayfield.BASE_SIZE / 2,
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(Vector2.Zero),
+ new PathControlPoint(new Vector2(100, 0)),
+ })
+ },
+ new HitCircle
+ {
+ StartTime = 4500,
+ Position = OsuPlayfield.BASE_SIZE / 2,
+ },
+ },
+ };
+ }
+ );
+
+ AddUntilStep("fetch cursor particles", () =>
+ {
+ cursorParticles = this.ChildrenOfType().SingleOrDefault();
+ return cursorParticles != null;
+ });
+
+ AddUntilStep("wait for spinner tracking", () =>
+ {
+ spinner = this.ChildrenOfType().SingleOrDefault();
+ return spinner?.RotationTracker.Tracking == true;
+ });
+ AddAssert("particles are being spawned", () => cursorParticles.Active);
+
+ AddUntilStep("spinner tracking stopped", () => !spinner.RotationTracker.Tracking);
+ AddAssert("particle spawning stopped", () => !cursorParticles.Active);
+
+ AddUntilStep("wait for slider tracking", () =>
+ {
+ slider = this.ChildrenOfType().SingleOrDefault();
+ return slider?.Tracking.Value == true;
+ });
+ AddAssert("particles are being spawned", () => cursorParticles.Active);
+
+ AddUntilStep("slider tracking stopped", () => !slider.Tracking.Value);
+ AddAssert("particle spawning stopped", () => !cursorParticles.Active);
+ }
+
+ private void createLegacyTest(bool autoplay, Func beatmap) => CreateTest(() =>
+ {
+ AddStep("set beatmap", () =>
+ {
+ this.autoplay = autoplay;
+ currentBeatmap = beatmap();
+ });
+ AddStep("setup default legacy skin", () =>
+ {
+ skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
+ });
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 2326a0c391..f9dc9abd75 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
+using osu.Framework.Input.Events;
using osu.Framework.Testing.Input;
using osu.Framework.Utils;
using osu.Game.Audio;
@@ -143,9 +144,9 @@ namespace osu.Game.Rulesets.Osu.Tests
pressed = value;
if (value)
- OnPressed(OsuAction.LeftButton);
+ OnPressed(new KeyBindingPressEvent(GetContainingInputManager().CurrentState, OsuAction.LeftButton));
else
- OnReleased(OsuAction.LeftButton);
+ OnReleased(new KeyBindingReleaseEvent(GetContainingInputManager().CurrentState, OsuAction.LeftButton));
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
index 1fdcd73dde..575523b168 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
@@ -4,6 +4,7 @@
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
@@ -97,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void scheduleHit() => AddStep("schedule action", () =>
{
var delay = hitCircle.StartTime - hitCircle.HitWindows.WindowFor(HitResult.Great) - Time.Current;
- Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(OsuAction.LeftButton), delay);
+ Scheduler.AddDelayed(() => hitAreaReceptor.OnPressed(new KeyBindingPressEvent(GetContainingInputManager().CurrentState, OsuAction.LeftButton)), delay);
});
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 662cbaee68..0f362851a9 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -20,6 +20,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
@@ -86,9 +87,9 @@ namespace osu.Game.Rulesets.Osu.Tests
if (firstObject == null)
return false;
- var skinnable = firstObject.ApproachCircle.Child as SkinnableDrawable;
+ var skinnable = firstObject.ApproachCircle;
- if (skin == null && skinnable?.Drawable is Sprite)
+ if (skin == null && skinnable?.Drawable is DefaultApproachCircle)
// check for default skin provider
return true;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
index 141138c125..ac77a93239 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public double AimStrain { get; set; }
public double SpeedStrain { get; set; }
+ public double FlashlightRating { get; set; }
public double ApproachRate { get; set; }
public double OverallDifficulty { get; set; }
public int HitCircleCount { get; set; }
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index e47f82fb39..4c8d0b2ce6 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public class OsuDifficultyCalculator : DifficultyCalculator
{
private const double difficulty_multiplier = 0.0675;
+ private double hitWindowGreat;
public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
@@ -34,13 +35,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
- double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;
+ double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
- HitWindows hitWindows = new OsuHitWindows();
- hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+ double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
+ double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
+ double baseFlashlightPerformance = 0.0;
+
+ if (mods.Any(h => h is OsuModFlashlight))
+ baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0;
+
+ double basePerformance =
+ Math.Pow(
+ Math.Pow(baseAimPerformance, 1.1) +
+ Math.Pow(baseSpeedPerformance, 1.1) +
+ Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
+ );
+
+ double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
- // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
- double hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
int maxCombo = beatmap.HitObjects.Count;
@@ -56,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
Mods = mods,
AimStrain = aimRating,
SpeedStrain = speedRating,
+ FlashlightRating = flashlightRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
MaxCombo = maxCombo,
@@ -79,11 +92,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
}
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
- new Aim(mods),
- new Speed(mods)
- };
+ HitWindows hitWindows = new OsuHitWindows();
+ hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
+
+ // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
+ hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
+
+ return new Skill[]
+ {
+ new Aim(mods),
+ new Speed(mods, hitWindowGreat),
+ new Flashlight(mods)
+ };
+ }
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
@@ -91,6 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
new OsuModHalfTime(),
new OsuModEasy(),
new OsuModHardRock(),
+ new OsuModFlashlight(),
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index e6ab978dfb..bf4d92652c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
// Custom multipliers for NoFail and SpunOut.
- double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
+ double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
@@ -52,11 +52,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
+ double flashlightValue = computeFlashlightValue();
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1) +
Math.Pow(speedValue, 1.1) +
- Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
+ Math.Pow(accuracyValue, 1.1) +
+ Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryRatings != null)
@@ -64,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
categoryRatings.Add("Aim", aimValue);
categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue);
+ categoryRatings.Add("Flashlight", flashlightValue);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
@@ -81,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
- // Longer maps are worth more
+ // Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@@ -91,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (countMiss > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
- // Combo scaling
+ // Combo scaling.
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
@@ -109,23 +112,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
- double flashlightBonus = 1.0;
+ aimValue *= approachRateBonus;
- if (mods.Any(h => h is OsuModFlashlight))
- {
- // Apply object-based bonus for flashlight.
- flashlightBonus = 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
- (totalHits > 200
- ? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
- (totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
- : 0.0);
- }
-
- aimValue *= Math.Max(flashlightBonus, approachRateBonus);
-
- // Scale the aim value with accuracy _slightly_
+ // Scale the aim value with accuracy _slightly_.
aimValue *= 0.5 + accuracy / 2.0;
- // It is important to also consider accuracy difficulty when doing that
+ // It is important to also consider accuracy difficulty when doing that.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return aimValue;
@@ -135,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
- // Longer maps are worth more
+ // Longer maps are worth more.
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus;
@@ -144,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (countMiss > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
- // Combo scaling
+ // Combo scaling.
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
@@ -159,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
- // Scale the speed value with accuracy and OD
+ // Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@@ -169,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAccuracyValue()
{
- // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
+ // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
@@ -178,15 +169,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
else
betterAccuracyPercentage = 0;
- // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points
+ // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
if (betterAccuracyPercentage < 0)
betterAccuracyPercentage = 0;
// Lots of arbitrary values from testing.
- // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
+ // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
- // Bonus for many hitcircles - it's harder to keep good accuracy up for longer
+ // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
if (mods.Any(m => m is OsuModHidden))
@@ -197,6 +188,42 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return accuracyValue;
}
+ private double computeFlashlightValue()
+ {
+ if (!mods.Any(h => h is OsuModFlashlight))
+ return 0.0;
+
+ double rawFlashlight = Attributes.FlashlightRating;
+
+ if (mods.Any(m => m is OsuModTouchDevice))
+ rawFlashlight = Math.Pow(rawFlashlight, 0.8);
+
+ double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
+
+ // Add an additional bonus for HDFL.
+ if (mods.Any(h => h is OsuModHidden))
+ flashlightValue *= 1.3;
+
+ // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
+ if (countMiss > 0)
+ flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
+
+ // Combo scaling.
+ if (Attributes.MaxCombo > 0)
+ flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
+
+ // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
+ flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
+ (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
+
+ // Scale the flashlight value with accuracy _slightly_.
+ flashlightValue *= 0.5 + accuracy / 2.0;
+ // It is important to also consider accuracy difficulty when doing that.
+ flashlightValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
+
+ return flashlightValue;
+ }
+
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index fa6c5c4d9c..8e8f9bc06e 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -16,6 +16,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
+ ///
+ /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms to account for simultaneous s.
+ ///
+ public double StrainTime { get; private set; }
+
///
/// Normalized distance from the end position of the previous to the start position of this .
///
@@ -32,11 +37,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
///
public double? Angle { get; private set; }
- ///
- /// Milliseconds elapsed since the start time of the previous , with a minimum of 50ms.
- ///
- public readonly double StrainTime;
-
private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject;
@@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
setDistances();
- // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
- StrainTime = Math.Max(50, DeltaTime);
+ // Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects.
+ StrainTime = Math.Max(DeltaTime, 25);
}
private void setDistances()
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
new file mode 100644
index 0000000000..abd900a80d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
@@ -0,0 +1,66 @@
+// 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.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Difficulty.Skills
+{
+ ///
+ /// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled.
+ ///
+ public class Flashlight : OsuStrainSkill
+ {
+ public Flashlight(Mod[] mods)
+ : base(mods)
+ {
+ }
+
+ protected override double SkillMultiplier => 0.15;
+ protected override double StrainDecayBase => 0.15;
+ protected override double DecayWeight => 1.0;
+ protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
+
+ protected override double StrainValueOf(DifficultyHitObject current)
+ {
+ if (current.BaseObject is Spinner)
+ return 0;
+
+ var osuCurrent = (OsuDifficultyHitObject)current;
+ var osuHitObject = (OsuHitObject)(osuCurrent.BaseObject);
+
+ double scalingFactor = 52.0 / osuHitObject.Radius;
+ double smallDistNerf = 1.0;
+ double cumulativeStrainTime = 0.0;
+
+ double result = 0.0;
+
+ for (int i = 0; i < Previous.Count; i++)
+ {
+ var osuPrevious = (OsuDifficultyHitObject)Previous[i];
+ var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject);
+
+ if (!(osuPrevious.BaseObject is Spinner))
+ {
+ double jumpDistance = (osuHitObject.StackedPosition - osuPreviousHitObject.EndPosition).Length;
+
+ cumulativeStrainTime += osuPrevious.StrainTime;
+
+ // We want to nerf objects that can be easily seen within the Flashlight circle radius.
+ if (i == 0)
+ smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
+
+ // We also want to nerf stacks so that only the first object of the stack is accounted for.
+ double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0);
+
+ result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
+ }
+ }
+
+ return Math.Pow(smallDistNerf * result, 2.0);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index f0eb199e5f..9364b11048 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -6,6 +6,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
@@ -26,12 +27,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double DifficultyMultiplier => 1.04;
private const double min_speed_bonus = 75; // ~200BPM
- private const double max_speed_bonus = 45; // ~330BPM
private const double speed_balancing_factor = 40;
- public Speed(Mod[] mods)
+ private readonly double greatWindow;
+
+ public Speed(Mod[] mods, double hitWindowGreat)
: base(mods)
{
+ greatWindow = hitWindowGreat;
}
protected override double StrainValueOf(DifficultyHitObject current)
@@ -40,13 +43,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return 0;
var osuCurrent = (OsuDifficultyHitObject)current;
+ var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance);
- double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime);
+ double strainTime = osuCurrent.StrainTime;
+
+ double greatWindowFull = greatWindow * 2;
+ double speedWindowRatio = strainTime / greatWindowFull;
+
+ // Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
+ if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime)
+ strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio);
+
+ // Cap deltatime to the OD 300 hitwindow.
+ // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
+ strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
double speedBonus = 1.0;
- if (deltaTime < min_speed_bonus)
- speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2);
+ if (strainTime < min_speed_bonus)
+ speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
double angleBonus = 1.0;
@@ -64,7 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
}
}
- return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime;
+ return (1 + (speedBonus - 1) * 0.75)
+ * angleBonus
+ * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5))
+ / strainTime;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 6269a41350..1be9b5bf2e 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -127,9 +127,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return false;
}
- public bool OnPressed(PlatformAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- switch (action)
+ switch (e.Action)
{
case PlatformAction.Delete:
return DeleteSelected();
@@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return false;
}
- public void OnReleased(PlatformAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 806b7e6051..1e84ec80e1 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -42,10 +42,12 @@ namespace osu.Game.Rulesets.Osu.Edit
};
private readonly Bindable distanceSnapToggle = new Bindable();
+ private readonly Bindable rectangularGridSnapToggle = new Bindable();
protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{
- new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
+ new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }),
+ new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
});
private BindableList selectedHitObjects;
@@ -63,6 +65,10 @@ namespace osu.Game.Rulesets.Osu.Edit
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
},
distanceSnapGridContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid
{
RelativeSizeAxes = Axes.Both
}
@@ -73,7 +79,19 @@ namespace osu.Game.Rulesets.Osu.Edit
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
- distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
+ distanceSnapToggle.ValueChanged += _ =>
+ {
+ updateDistanceSnapGrid();
+
+ if (distanceSnapToggle.Value == TernaryState.True)
+ rectangularGridSnapToggle.Value = TernaryState.False;
+ };
+
+ rectangularGridSnapToggle.ValueChanged += _ =>
+ {
+ if (rectangularGridSnapToggle.Value == TernaryState.True)
+ distanceSnapToggle.Value = TernaryState.False;
+ };
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
@@ -91,6 +109,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Cached distanceSnapGridCache = new Cached();
private double? lastDistanceSnapGridTime;
+ private RectangularPositionSnapGrid rectangularPositionSnapGrid;
+
protected override void Update()
{
base.Update();
@@ -122,13 +142,19 @@ namespace osu.Game.Rulesets.Osu.Edit
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
return positionSnap;
- // will be null if distance snap is disabled or not feasible for the current time value.
- if (distanceSnapGrid == null)
- return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
+ if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
+ {
+ (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
+ return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ }
- (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
+ if (rectangularGridSnapToggle.Value == TernaryState.True)
+ {
+ Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
+ return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ }
- return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
}
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs
new file mode 100644
index 0000000000..b8ff92bd37
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs
@@ -0,0 +1,69 @@
+// 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.Framework.Allocation;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Input.Bindings;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class OsuRectangularPositionSnapGrid : RectangularPositionSnapGrid, IKeyBindingHandler
+ {
+ private static readonly int[] grid_sizes = { 4, 8, 16, 32 };
+
+ private int currentGridSizeIndex = grid_sizes.Length - 1;
+
+ [Resolved]
+ private EditorBeatmap editorBeatmap { get; set; }
+
+ public OsuRectangularPositionSnapGrid()
+ : base(OsuPlayfield.BASE_SIZE / 2)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize);
+ if (gridSizeIndex >= 0)
+ currentGridSizeIndex = gridSizeIndex;
+ updateSpacing();
+ }
+
+ private void nextGridSize()
+ {
+ currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length;
+ updateSpacing();
+ }
+
+ private void updateSpacing()
+ {
+ int gridSize = grid_sizes[currentGridSizeIndex];
+
+ editorBeatmap.BeatmapInfo.GridSize = gridSize;
+ Spacing = new Vector2(gridSize);
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ switch (e.Action)
+ {
+ case GlobalAction.EditorCycleGridDisplayMode:
+ nextGridSize();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 46fc8f99b2..f05aea0df4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
@@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public OsuAction? HitAction => HitArea.HitAction;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
- public ApproachCircle ApproachCircle { get; private set; }
+ public SkinnableDrawable ApproachCircle { get; private set; }
public HitReceptor HitArea { get; private set; }
public SkinnableDrawable CirclePiece { get; private set; }
@@ -74,8 +75,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
- ApproachCircle = new ApproachCircle
+ ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
{
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
Alpha = 0,
Scale = new Vector2(4),
}
@@ -88,7 +92,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
- AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue);
}
protected override void LoadComplete()
@@ -228,15 +231,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
CornerExponent = 2;
}
- public bool OnPressed(OsuAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- switch (action)
+ switch (e.Action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
if (IsHovered && (Hit?.Invoke() ?? false))
{
- HitAction = action;
+ HitAction = e.Action;
return true;
}
@@ -246,7 +249,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return false;
}
- public void OnReleased(OsuAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+
+ private class ProxyableSkinnableDrawable : SkinnableDrawable
+ {
+ public override bool RemoveWhenNotAlive => false;
+
+ public ProxyableSkinnableDrawable(ISkinComponent component, Func defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
+ : base(component, defaultImplementation, confineMode)
{
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 0bec33bf77..3acec4498d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -32,6 +32,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SliderBall Ball { get; private set; }
public SkinnableDrawable Body { get; private set; }
+ ///
+ /// A target container which can be used to add top level elements to the slider's display.
+ /// Intended to be used for proxy purposes only.
+ ///
+ public Container OverlayElementContainer { get; private set; }
+
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
[CanBeNull]
@@ -65,6 +71,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
tailContainer = new Container { RelativeSizeAxes = Axes.Both },
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
+ headContainer = new Container { RelativeSizeAxes = Axes.Both },
+ OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
Ball = new SliderBall(this)
{
GetInitialHitAction = () => HeadCircle.HitAction,
@@ -72,7 +80,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true,
Alpha = 0
},
- headContainer = new Container { RelativeSizeAxes = Axes.Both },
slidingSample = new PausableSkinnableSound { Looping = true }
};
@@ -179,6 +186,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
tailContainer.Clear(false);
repeatContainer.Clear(false);
tickContainer.Clear(false);
+
+ OverlayElementContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index 01c0d988ee..2b026e6840 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[CanBeNull]
public Slider Slider => DrawableSlider?.HitObject;
- protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
+ public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 4a2a18ffd6..673211ac6c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[CanBeNull]
public Slider Slider => DrawableSlider?.HitObject;
- protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
+ public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
private double animDuration;
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index 46e501758b..71657ed532 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu
FollowPoint,
Cursor,
CursorTrail,
+ CursorParticles,
SliderScorePoint,
ReverseArrow,
HitCircleText,
@@ -18,5 +19,6 @@ namespace osu.Game.Rulesets.Osu
SliderBall,
SliderBody,
SpinnerBody,
+ ApproachCircle,
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs
deleted file mode 100644
index 62f00a2b49..0000000000
--- a/osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Textures;
-using osu.Game.Skinning;
-using osuTK;
-
-namespace osu.Game.Rulesets.Osu.Skinning.Default
-{
- public class ApproachCircle : Container
- {
- public override bool RemoveWhenNotAlive => false;
-
- public ApproachCircle()
- {
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
-
- RelativeSizeAxes = Axes.Both;
- }
-
- [BackgroundDependencyLoader]
- private void load(TextureStore textures)
- {
- Child = new SkinnableApproachCircle();
- }
-
- private class SkinnableApproachCircle : SkinnableSprite
- {
- public SkinnableApproachCircle()
- : base("Gameplay/osu/approachcircle")
- {
- }
-
- protected override Drawable CreateDefault(ISkinComponent component)
- {
- var drawable = base.CreateDefault(component);
-
- // account for the sprite being used for the default approach circle being taken from stable,
- // when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
- drawable.Scale = new Vector2(128 / 118f);
-
- return drawable;
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs
new file mode 100644
index 0000000000..a522367fe6
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultApproachCircle.cs
@@ -0,0 +1,49 @@
+// 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.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public class DefaultApproachCircle : SkinnableSprite
+ {
+ private readonly IBindable accentColour = new Bindable();
+
+ [Resolved]
+ private DrawableHitObject drawableObject { get; set; }
+
+ public DefaultApproachCircle()
+ : base("Gameplay/osu/approachcircle")
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ accentColour.BindTo(drawableObject.AccentColour);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ accentColour.BindValueChanged(colour => Colour = colour.NewValue, true);
+ }
+
+ protected override Drawable CreateDefault(ISkinComponent component)
+ {
+ var drawable = base.CreateDefault(component);
+
+ // Although this is a non-legacy component, osu-resources currently stores approach circle as a legacy-like texture.
+ // See LegacyApproachCircle for documentation as to why this is required.
+ drawable.Scale = new Vector2(128 / 118f);
+
+ return drawable;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs
new file mode 100644
index 0000000000..6a2cb871b1
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs
@@ -0,0 +1,49 @@
+// 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.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+{
+ public class LegacyApproachCircle : SkinnableSprite
+ {
+ private readonly IBindable accentColour = new Bindable();
+
+ [Resolved]
+ private DrawableHitObject drawableObject { get; set; }
+
+ public LegacyApproachCircle()
+ : base("Gameplay/osu/approachcircle")
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ accentColour.BindTo(drawableObject.AccentColour);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ accentColour.BindValueChanged(colour => Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
+ }
+
+ protected override Drawable CreateDefault(ISkinComponent component)
+ {
+ var drawable = base.CreateDefault(component);
+
+ // account for the sprite being used for the default approach circle being taken from stable,
+ // when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
+ drawable.Scale = new Vector2(128 / 118f);
+
+ return drawable;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
new file mode 100644
index 0000000000..c2db5f3f82
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
@@ -0,0 +1,253 @@
+// 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.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+{
+ public class LegacyCursorParticles : CompositeDrawable, IKeyBindingHandler
+ {
+ public bool Active => breakSpewer?.Active.Value == true || kiaiSpewer?.Active.Value == true;
+
+ private LegacyCursorParticleSpewer breakSpewer;
+ private LegacyCursorParticleSpewer kiaiSpewer;
+
+ [Resolved(canBeNull: true)]
+ private Player player { get; set; }
+
+ [Resolved(canBeNull: true)]
+ private OsuPlayfield playfield { get; set; }
+
+ [Resolved(canBeNull: true)]
+ private GameplayBeatmap gameplayBeatmap { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, OsuColour colours)
+ {
+ var texture = skin.GetTexture("star2");
+ var starBreakAdditive = skin.GetConfig(OsuSkinColour.StarBreakAdditive)?.Value ?? new Color4(255, 182, 193, 255);
+
+ if (texture != null)
+ {
+ // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
+ texture.ScaleAdjust *= 1.6f;
+ }
+
+ InternalChildren = new[]
+ {
+ breakSpewer = new LegacyCursorParticleSpewer(texture, 20)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = starBreakAdditive,
+ Direction = SpewDirection.None,
+ },
+ kiaiSpewer = new LegacyCursorParticleSpewer(texture, 60)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = starBreakAdditive,
+ Direction = SpewDirection.None,
+ },
+ };
+
+ if (player != null)
+ ((IBindable)breakSpewer.Active).BindTo(player.IsBreakTime);
+ }
+
+ protected override void Update()
+ {
+ if (playfield == null || gameplayBeatmap == null) return;
+
+ DrawableHitObject kiaiHitObject = null;
+
+ // Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary.
+ if (gameplayBeatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode)
+ kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking);
+
+ kiaiSpewer.Active.Value = kiaiHitObject != null;
+ }
+
+ private bool isTracking(DrawableHitObject h)
+ {
+ if (!h.HitObject.Kiai)
+ return false;
+
+ switch (h)
+ {
+ case DrawableSlider slider:
+ return slider.Tracking.Value;
+
+ case DrawableSpinner spinner:
+ return spinner.RotationTracker.Tracking;
+ }
+
+ return false;
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ handleInput(e.Action, true);
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ handleInput(e.Action, false);
+ }
+
+ private bool leftPressed;
+ private bool rightPressed;
+
+ private void handleInput(OsuAction action, bool pressed)
+ {
+ switch (action)
+ {
+ case OsuAction.LeftButton:
+ leftPressed = pressed;
+ break;
+
+ case OsuAction.RightButton:
+ rightPressed = pressed;
+ break;
+ }
+
+ if (leftPressed && rightPressed)
+ breakSpewer.Direction = SpewDirection.Omni;
+ else if (leftPressed)
+ breakSpewer.Direction = SpewDirection.Left;
+ else if (rightPressed)
+ breakSpewer.Direction = SpewDirection.Right;
+ else
+ breakSpewer.Direction = SpewDirection.None;
+ }
+
+ private class LegacyCursorParticleSpewer : ParticleSpewer, IRequireHighFrequencyMousePosition
+ {
+ private const int particle_duration_min = 300;
+ private const int particle_duration_max = 1000;
+
+ public SpewDirection Direction { get; set; }
+
+ protected override bool CanSpawnParticles => base.CanSpawnParticles && cursorScreenPosition.HasValue;
+ protected override float ParticleGravity => 240;
+
+ public LegacyCursorParticleSpewer(Texture texture, int perSecond)
+ : base(texture, perSecond, particle_duration_max)
+ {
+ Active.BindValueChanged(_ => resetVelocityCalculation());
+ }
+
+ private Vector2? cursorScreenPosition;
+ private Vector2 cursorVelocity;
+
+ private const double max_velocity_frame_length = 15;
+ private double velocityFrameLength;
+ private Vector2 totalPosDifference;
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ if (cursorScreenPosition == null)
+ {
+ cursorScreenPosition = e.ScreenSpaceMousePosition;
+ return base.OnMouseMove(e);
+ }
+
+ // calculate cursor velocity.
+ totalPosDifference += e.ScreenSpaceMousePosition - cursorScreenPosition.Value;
+ cursorScreenPosition = e.ScreenSpaceMousePosition;
+
+ velocityFrameLength += Math.Abs(Clock.ElapsedFrameTime);
+
+ if (velocityFrameLength > max_velocity_frame_length)
+ {
+ cursorVelocity = totalPosDifference / (float)velocityFrameLength;
+
+ totalPosDifference = Vector2.Zero;
+ velocityFrameLength = 0;
+ }
+
+ return base.OnMouseMove(e);
+ }
+
+ private void resetVelocityCalculation()
+ {
+ cursorScreenPosition = null;
+ totalPosDifference = Vector2.Zero;
+ velocityFrameLength = 0;
+ }
+
+ protected override FallingParticle CreateParticle() =>
+ new FallingParticle
+ {
+ StartPosition = ToLocalSpace(cursorScreenPosition ?? Vector2.Zero),
+ Duration = RNG.NextSingle(particle_duration_min, particle_duration_max),
+ StartAngle = (float)(RNG.NextDouble() * 4 - 2),
+ EndAngle = RNG.NextSingle(-2f, 2f),
+ EndScale = RNG.NextSingle(2f),
+ Velocity = getVelocity(),
+ };
+
+ private Vector2 getVelocity()
+ {
+ Vector2 velocity = Vector2.Zero;
+
+ switch (Direction)
+ {
+ case SpewDirection.Left:
+ velocity = new Vector2(
+ RNG.NextSingle(-460f, 0),
+ RNG.NextSingle(-40f, 40f)
+ );
+ break;
+
+ case SpewDirection.Right:
+ velocity = new Vector2(
+ RNG.NextSingle(0, 460f),
+ RNG.NextSingle(-40f, 40f)
+ );
+ break;
+
+ case SpewDirection.Omni:
+ velocity = new Vector2(
+ RNG.NextSingle(-460f, 460f),
+ RNG.NextSingle(-160f, 160f)
+ );
+ break;
+ }
+
+ velocity += cursorVelocity * 40;
+
+ return velocity;
+ }
+ }
+
+ private enum SpewDirection
+ {
+ None,
+ Left,
+ Right,
+ Omni,
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index 7a210324d7..3afd814174 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
- private Container circleSprites;
private Drawable hitCircleSprite;
- private Drawable hitCircleOverlay;
+
+ protected Drawable HitCircleOverlay { get; private set; }
private SkinnableSpriteText hitCircleText;
@@ -70,28 +70,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
Texture overlayTexture = getTextureWithFallback("overlay");
- InternalChildren = new Drawable[]
+ InternalChildren = new[]
{
- circleSprites = new Container
+ hitCircleSprite = new KiaiFlashingSprite
{
+ Texture = baseTexture,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ HitCircleOverlay = new KiaiFlashingSprite
+ {
+ Texture = overlayTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Children = new[]
- {
- hitCircleSprite = new KiaiFlashingSprite
- {
- Texture = baseTexture,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- hitCircleOverlay = new KiaiFlashingSprite
- {
- Texture = overlayTexture,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }
- }
},
};
@@ -111,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (overlayAboveNumber)
- AddInternal(hitCircleOverlay.CreateProxy());
+ ChangeInternalChildDepth(HitCircleOverlay, float.MinValue);
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
@@ -153,8 +144,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (state)
{
case ArmedState.Hit:
- circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
- circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+
+ HitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
+ HitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber)
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
new file mode 100644
index 0000000000..7a071b5a03
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+{
+ public class LegacyReverseArrow : CompositeDrawable
+ {
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject drawableHitObject { get; set; }
+
+ private Drawable proxy;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skinSource)
+ {
+ AutoSizeAxes = Axes.Both;
+
+ string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName;
+
+ var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
+ InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ proxy = CreateProxy();
+
+ if (drawableHitObject != null)
+ {
+ drawableHitObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(drawableHitObject);
+ }
+ }
+
+ private void onHitObjectApplied(DrawableHitObject drawableObject)
+ {
+ Debug.Assert(proxy.Parent == null);
+
+ // see logic in LegacySliderHeadHitCircle.
+ (drawableObject as DrawableSliderRepeat)?.DrawableSlider
+ .OverlayElementContainer.Add(proxy);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ if (drawableHitObject != null)
+ drawableHitObject.HitObjectApplied -= onHitObjectApplied;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs
new file mode 100644
index 0000000000..13ba42ba50
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+{
+ public class LegacySliderHeadHitCircle : LegacyMainCirclePiece
+ {
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject drawableHitObject { get; set; }
+
+ private Drawable proxiedHitCircleOverlay;
+
+ public LegacySliderHeadHitCircle()
+ : base("sliderstartcircle")
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ proxiedHitCircleOverlay = HitCircleOverlay.CreateProxy();
+
+ if (drawableHitObject != null)
+ {
+ drawableHitObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(drawableHitObject);
+ }
+ }
+
+ private void onHitObjectApplied(DrawableHitObject drawableObject)
+ {
+ Debug.Assert(proxiedHitCircleOverlay.Parent == null);
+
+ // see logic in LegacyReverseArrow.
+ (drawableObject as DrawableSliderHead)?.DrawableSlider
+ .OverlayElementContainer.Add(proxiedHitCircleOverlay.With(d => d.Depth = float.MinValue));
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableHitObject != null)
+ drawableHitObject.HitObjectApplied -= onHitObjectApplied;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 41b0a88f11..ff9f6f0e07 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -67,7 +67,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
- return new LegacyMainCirclePiece("sliderstartcircle");
+ return new LegacySliderHeadHitCircle();
+
+ return null;
+
+ case OsuSkinComponents.ReverseArrow:
+ if (hasHitCircle.Value)
+ return new LegacyReverseArrow();
return null;
@@ -89,6 +95,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
+ case OsuSkinComponents.CursorParticles:
+ if (GetTexture("star2") != null)
+ return new LegacyCursorParticles();
+
+ return null;
+
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null;
@@ -108,6 +120,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return new LegacyOldStyleSpinner();
return null;
+
+ case OsuSkinComponents.ApproachCircle:
+ return new LegacyApproachCircle();
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs
index f7ba8b9fc4..24f9217a5f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs
@@ -9,5 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
SliderBorder,
SliderBall,
SpinnerBackground,
+ StarBreakAdditive,
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index 5812e8cf75..83bcc88e5f 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
@@ -42,7 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
InternalChild = fadeContainer = new Container
{
RelativeSizeAxes = Axes.Both,
- Child = cursorTrail = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling)
+ Children = new[]
+ {
+ cursorTrail = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
+ new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
+ }
};
}
@@ -115,9 +120,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
(ActiveCursor as OsuCursor)?.Contract();
}
- public bool OnPressed(OsuAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- switch (action)
+ switch (e.Action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
@@ -129,9 +134,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
return false;
}
- public void OnReleased(OsuAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
- switch (action)
+ switch (e.Action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index c1c2ea2299..2233a547b9 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -24,6 +24,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
+ [Cached]
public class OsuPlayfield : Playfield
{
private readonly PlayfieldBorder playfieldBorder;
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index 27d48d1296..4d4340936d 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.Osu.UI
base.OnHoverLost(e);
}
- public bool OnPressed(OsuAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- switch (action)
+ switch (e.Action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
@@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.UI
return false;
}
- public void OnReleased(OsuAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
index f048cad18c..6d4cac0ebe 100644
--- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.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 osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring;
@@ -37,6 +38,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
Result.Type = Type;
}
- public override bool OnPressed(TaikoAction action) => false;
+ public override bool OnPressed(KeyBindingPressEvent e) => false;
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs
index 829bcf34a1..ea877c9e17 100644
--- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
@@ -30,6 +31,6 @@ namespace osu.Game.Rulesets.Taiko.Tests
nestedStrongHit.Result.Type = hitBoth ? Type : HitResult.Miss;
}
- public override bool OnPressed(TaikoAction action) => false;
+ public override bool OnPressed(KeyBindingPressEvent e) => false;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index 1253b7c8ae..0a325f174e 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -36,24 +36,27 @@ namespace osu.Game.Rulesets.Taiko.Mods
public TaikoFlashlight(TaikoPlayfield taikoPlayfield)
{
this.taikoPlayfield = taikoPlayfield;
- FlashlightSize = new Vector2(0, getSizeFor(0));
+ FlashlightSize = getSizeFor(0);
AddLayout(flashlightProperties);
}
- private float getSizeFor(int combo)
+ private Vector2 getSizeFor(int combo)
{
+ float size = default_flashlight_size;
+
if (combo > 200)
- return default_flashlight_size * 0.8f;
+ size *= 0.8f;
else if (combo > 100)
- return default_flashlight_size * 0.9f;
- else
- return default_flashlight_size;
+ size *= 0.9f;
+
+ // Preserve flashlight size through the playfield's aspect adjustment.
+ return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
}
protected override void OnComboChange(ValueChangedEvent e)
{
- this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), getSizeFor(e.NewValue), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
@@ -64,7 +67,11 @@ namespace osu.Game.Rulesets.Taiko.Mods
if (!flashlightProperties.IsValid)
{
- FlashlightPosition = taikoPlayfield.HitTarget.ToSpaceOfOtherDrawable(taikoPlayfield.HitTarget.OriginPosition, this);
+ FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
+
+ ClearTransforms(targetMember: nameof(FlashlightSize));
+ FlashlightSize = getSizeFor(Combo.Value);
+
flashlightProperties.Validate();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
index ba6a2f2d66..7f565cb82d 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
@@ -2,22 +2,40 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
- public class TaikoModHidden : ModHidden
+ public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset
{
public override string Description => @"Beats fade out before you hit them!";
public override double ScoreMultiplier => 1.06;
- private ControlPointInfo controlPointInfo;
+ ///
+ /// How far away from the hit target should hitobjects start to fade out.
+ /// Range: [0, 1]
+ ///
+ private const float fade_out_start_time = 1f;
+
+ ///
+ /// How long hitobjects take to fade out, in terms of the scrolling length.
+ /// Range: [0, 1]
+ ///
+ private const float fade_out_duration = 0.375f;
+
+ private DrawableTaikoRuleset drawableRuleset;
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ this.drawableRuleset = (DrawableTaikoRuleset)drawableRuleset;
+ }
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@@ -30,9 +48,9 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
case DrawableDrumRollTick _:
case DrawableHit _:
- double preempt = 10000 / MultiplierAt(hitObject.HitObject);
- double start = hitObject.HitObject.StartTime - preempt * 0.6;
- double duration = preempt * 0.3;
+ double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier;
+ double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time;
+ double duration = preempt * fade_out_duration;
using (hitObject.BeginAbsoluteSequence(start))
{
@@ -48,17 +66,5 @@ namespace osu.Game.Rulesets.Taiko.Mods
break;
}
}
-
- protected double MultiplierAt(HitObject obj)
- {
- double beatLength = controlPointInfo.TimingPointAt(obj.StartTime).BeatLength;
-
- return obj.DifficultyControlPoint.SliderVelocity * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength;
- }
-
- public override void ApplyToBeatmap(IBeatmap beatmap)
- {
- controlPointInfo = beatmap.ControlPointInfo;
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index d066abf767..521189d36c 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
@@ -112,7 +113,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollBody),
_ => new ElongatedCirclePiece());
- public override bool OnPressed(TaikoAction action) => false;
+ public override bool OnPressed(KeyBindingPressEvent e) => false;
private void onNewResult(DrawableHitObject obj, JudgementResult result)
{
@@ -196,7 +197,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
- public override bool OnPressed(TaikoAction action) => false;
+ public override bool OnPressed(KeyBindingPressEvent e) => false;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 0df45c424d..dc2ed200a1 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -4,6 +4,7 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
@@ -61,9 +62,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
}
}
- public override bool OnPressed(TaikoAction action)
+ public override bool OnPressed(KeyBindingPressEvent e)
{
- JudgementType = action == TaikoAction.LeftRim || action == TaikoAction.RightRim ? HitType.Rim : HitType.Centre;
+ JudgementType = e.Action == TaikoAction.LeftRim || e.Action == TaikoAction.RightRim ? HitType.Rim : HitType.Centre;
return UpdateResult(true);
}
@@ -91,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
- public override bool OnPressed(TaikoAction action) => false;
+ public override bool OnPressed(KeyBindingPressEvent e) => false;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index 1e9fc187eb..97adc5f197 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -8,6 +8,7 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
@@ -145,19 +146,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
ApplyResult(r => r.Type = result);
}
- public override bool OnPressed(TaikoAction action)
+ public override bool OnPressed(KeyBindingPressEvent e)
{
if (pressHandledThisFrame)
return true;
if (Judged)
return false;
- validActionPressed = HitActions.Contains(action);
+ validActionPressed = HitActions.Contains(e.Action);
// Only count this as handled if the new judgement is a hit
var result = UpdateResult(true);
if (IsHit)
- HitAction = action;
+ HitAction = e.Action;
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
@@ -165,11 +166,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return result;
}
- public override void OnReleased(TaikoAction action)
+ public override void OnReleased(KeyBindingReleaseEvent e)
{
- if (action == HitAction)
+ if (e.Action == HitAction)
HitAction = null;
- base.OnReleased(action);
+ base.OnReleased(e);
}
protected override void Update()
@@ -265,7 +266,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
- public override bool OnPressed(TaikoAction action)
+ public override bool OnPressed(KeyBindingPressEvent e)
{
// Don't process actions until the main hitobject is hit
if (!ParentHitObject.IsHit)
@@ -276,7 +277,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return false;
// Don't handle invalid hit action presses
- if (!ParentHitObject.HitActions.Contains(action))
+ if (!ParentHitObject.HitActions.Contains(e.Action))
return false;
return UpdateResult(true);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index 888f47d341..2d19296d06 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -12,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning.Default;
@@ -266,13 +267,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private bool? lastWasCentre;
- public override bool OnPressed(TaikoAction action)
+ public override bool OnPressed(KeyBindingPressEvent e)
{
// Don't handle keys before the swell starts
if (Time.Current < HitObject.StartTime)
return false;
- var isCentre = action == TaikoAction.LeftCentre || action == TaikoAction.RightCentre;
+ var isCentre = e.Action == TaikoAction.LeftCentre || e.Action == TaikoAction.RightCentre;
// Ensure alternating centre and rim hits
if (lastWasCentre == isCentre)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
index 47fc7e5ab3..d4ea9ed29f 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
@@ -3,6 +3,7 @@
using JetBrains.Annotations;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
@@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
}
- public override bool OnPressed(TaikoAction action) => false;
+ public override bool OnPressed(KeyBindingPressEvent e) => false;
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.DrumRollTick),
_ => new TickPiece());
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 6a8d8a611c..eb64ba72f2 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
@@ -76,9 +77,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
public Drawable CreateProxiedContent() => proxiedContent.CreateProxy();
- public abstract bool OnPressed(TaikoAction action);
+ public abstract bool OnPressed(KeyBindingPressEvent e);
- public virtual void OnReleased(TaikoAction action)
+ public virtual void OnReleased(KeyBindingReleaseEvent e)
{
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
index 9d35093591..86be40dea8 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
@@ -141,16 +142,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
Centre.Texture = skin.GetTexture(@"taiko-drum-inner");
}
- public bool OnPressed(TaikoAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
Drawable target = null;
- if (action == CentreAction)
+ if (e.Action == CentreAction)
{
target = Centre;
sampleTriggerSource.Play(HitType.Centre);
}
- else if (action == RimAction)
+ else if (e.Action == RimAction)
{
target = Rim;
sampleTriggerSource.Play(HitType.Rim);
@@ -173,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return false;
}
- public void OnReleased(TaikoAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 6ddbf3c16b..824b95639b 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.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 System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -16,6 +17,7 @@ using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Scoring;
using osu.Game.Skinning;
@@ -60,6 +62,14 @@ namespace osu.Game.Rulesets.Taiko.UI
scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y;
}
+ public MultiplierControlPoint ControlPointAt(double time)
+ {
+ int result = ControlPoints.BinarySearch(new MultiplierControlPoint(time));
+ if (result < 0)
+ result = Math.Clamp(~result - 1, 0, ControlPoints.Count);
+ return ControlPoints[result];
+ }
+
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer();
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index ddfaf64549..861b800038 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
@@ -151,19 +152,19 @@ namespace osu.Game.Rulesets.Taiko.UI
[Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; }
- public bool OnPressed(TaikoAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
Drawable target = null;
Drawable back = null;
- if (action == CentreAction)
+ if (e.Action == CentreAction)
{
target = centreHit;
back = centre;
sampleTriggerSource.Play(HitType.Centre);
}
- else if (action == RimAction)
+ else if (e.Action == RimAction)
{
target = rimHit;
back = rim;
@@ -195,7 +196,7 @@ namespace osu.Game.Rulesets.Taiko.UI
return false;
}
- public void OnReleased(TaikoAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index ff9f585dbf..368d9d68df 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -64,6 +64,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsFalse(beatmapInfo.LetterboxInBreaks);
Assert.IsFalse(beatmapInfo.SpecialStyle);
Assert.IsFalse(beatmapInfo.WidescreenStoryboard);
+ Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset);
}
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index 5dc25d6643..b2bd60d342 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -424,14 +424,14 @@ namespace osu.Game.Tests.Beatmaps.IO
checkBeatmapCount(osu, 12);
checkSingleReferencedFileCount(osu, 18);
- var breakTemp = TestResources.GetTestBeatmapForImport();
+ var brokenTempFilename = TestResources.GetTestBeatmapForImport();
MemoryStream brokenOsu = new MemoryStream();
- MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(breakTemp));
+ MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(brokenTempFilename));
- File.Delete(breakTemp);
+ File.Delete(brokenTempFilename);
- using (var outStream = File.Open(breakTemp, FileMode.CreateNew))
+ using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
using (var zip = ZipArchive.Open(brokenOsz))
{
zip.AddEntry("broken.osu", brokenOsu, false);
@@ -441,7 +441,7 @@ namespace osu.Game.Tests.Beatmaps.IO
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
try
{
- await manager.Import(new ImportTask(breakTemp));
+ await manager.Import(new ImportTask(brokenTempFilename));
}
catch
{
@@ -456,6 +456,8 @@ namespace osu.Game.Tests.Beatmaps.IO
checkSingleReferencedFileCount(osu, 18);
Assert.AreEqual(1, loggedExceptionCount);
+
+ File.Delete(brokenTempFilename);
}
finally
{
diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
index 642ecf00b8..8be74f1a7c 100644
--- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
+++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
@@ -11,6 +11,7 @@ using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Input;
using osu.Game.Input.Bindings;
+using osu.Game.Rulesets;
using Realms;
namespace osu.Game.Tests.Database
@@ -42,7 +43,7 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer();
- keyBindingStore.Register(testContainer);
+ keyBindingStore.Register(testContainer, Enumerable.Empty());
Assert.That(queryCount(), Is.EqualTo(3));
@@ -66,7 +67,7 @@ namespace osu.Game.Tests.Database
{
KeyBindingContainer testContainer = new TestKeyBindingContainer();
- keyBindingStore.Register(testContainer);
+ keyBindingStore.Register(testContainer, Enumerable.Empty());
using (var primaryUsage = realmContextFactory.GetForRead())
{
diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
index 7a5789f01a..ce6b3a68a5 100644
--- a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
+++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Game.Online.API;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Mods
@@ -11,26 +12,42 @@ namespace osu.Game.Tests.Mods
public class ModSettingsEqualityComparison
{
[Test]
- public void Test()
+ public void TestAPIMod()
{
+ var apiMod1 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.25 } });
+ var apiMod2 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } });
+ var apiMod3 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } });
+
+ Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
+ Assert.That(apiMod2, Is.EqualTo(apiMod2));
+ Assert.That(apiMod2, Is.EqualTo(apiMod3));
+ Assert.That(apiMod3, Is.EqualTo(apiMod2));
+ }
+
+ [Test]
+ public void TestMod()
+ {
+ var ruleset = new OsuRuleset();
+
var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
- var apiMod1 = new APIMod(mod1);
- var apiMod2 = new APIMod(mod2);
- var apiMod3 = new APIMod(mod3);
+
+ var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset);
+ var doulbeConvertedMod2 = new APIMod(mod2).ToMod(ruleset);
+ var doulbeConvertedMod3 = new APIMod(mod3).ToMod(ruleset);
Assert.That(mod1, Is.Not.EqualTo(mod2));
- Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
+ Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doulbeConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod2));
- Assert.That(apiMod2, Is.EqualTo(apiMod2));
+ Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod3));
- Assert.That(apiMod2, Is.EqualTo(apiMod3));
+ Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod3));
Assert.That(mod3, Is.EqualTo(mod2));
- Assert.That(apiMod3, Is.EqualTo(apiMod2));
+ Assert.That(doulbeConvertedMod3, Is.EqualTo(doulbeConvertedMod2));
}
}
}
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
index e45b8f7dc5..785f31386d 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
@@ -40,10 +40,10 @@ namespace osu.Game.Tests.NonVisual.Skinning
assertPlaybackPosition(0);
AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
- assertPlaybackPosition(-1000);
+ assertPlaybackPosition(0);
AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
- assertPlaybackPosition(-500);
+ assertPlaybackPosition(0);
}
private void assertPlaybackPosition(double expectedPosition)
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 7170a76b8b..839366d98e 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -1,9 +1,11 @@
// 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.IO;
using NUnit.Framework;
using osu.Framework.IO.Stores;
+using osu.Framework.Testing;
namespace osu.Game.Tests.Resources
{
@@ -11,6 +13,8 @@ namespace osu.Game.Tests.Resources
{
public const double QUICK_BEATMAP_LENGTH = 10000;
+ private static readonly TemporaryNativeStorage temp_storage = new TemporaryNativeStorage("TestResources");
+
public static DllResourceStore GetStore() => new DllResourceStore(typeof(TestResources).Assembly);
public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}");
@@ -25,7 +29,7 @@ namespace osu.Game.Tests.Resources
/// A path to a copy of a beatmap archive (osz). Should be deleted after use.
public static string GetQuickTestBeatmapForImport()
{
- var tempPath = Path.GetTempFileName() + ".osz";
+ var tempPath = getTempFilename();
using (var stream = OpenResource("Archives/241526 Soleily - Renatus_virtual_quick.osz"))
using (var newFile = File.Create(tempPath))
stream.CopyTo(newFile);
@@ -41,7 +45,7 @@ namespace osu.Game.Tests.Resources
/// A path to a copy of a beatmap archive (osz). Should be deleted after use.
public static string GetTestBeatmapForImport(bool virtualTrack = false)
{
- var tempPath = Path.GetTempFileName() + ".osz";
+ var tempPath = getTempFilename();
using (var stream = GetTestBeatmapStream(virtualTrack))
using (var newFile = File.Create(tempPath))
@@ -50,5 +54,7 @@ namespace osu.Game.Tests.Resources
Assert.IsTrue(File.Exists(tempPath));
return tempPath;
}
+
+ private static string getTempFilename() => temp_storage.GetFullPath(Guid.NewGuid() + ".osz");
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs
new file mode 100644
index 0000000000..5d8a6dabd7
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs
@@ -0,0 +1,132 @@
+// 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.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneBlueprintOrdering : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ private EditorBlueprintContainer blueprintContainer
+ => Editor.ChildrenOfType().First();
+
+ [Test]
+ public void TestSelectedObjectHasPriorityWhenOverlapping()
+ {
+ var firstSlider = new Slider
+ {
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2()),
+ new PathControlPoint(new Vector2(150, -50)),
+ new PathControlPoint(new Vector2(300, 0))
+ }),
+ Position = new Vector2(0, 100)
+ };
+ var secondSlider = new Slider
+ {
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2()),
+ new PathControlPoint(new Vector2(-50, 50)),
+ new PathControlPoint(new Vector2(-100, 100))
+ }),
+ Position = new Vector2(200, 0)
+ };
+
+ AddStep("add overlapping sliders", () =>
+ {
+ EditorBeatmap.Add(firstSlider);
+ EditorBeatmap.Add(secondSlider);
+ });
+ AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(firstSlider));
+
+ AddStep("move mouse to common point", () =>
+ {
+ var pos = blueprintContainer.ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre;
+ InputManager.MoveMouseTo(pos);
+ });
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+
+ AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider);
+ }
+
+ [Test]
+ public void TestOverlappingObjectsWithSameStartTime()
+ {
+ AddStep("add overlapping circles", () =>
+ {
+ EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2));
+ EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2 + new Vector2(-10, -20)));
+ EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2 + new Vector2(10, -20)));
+ });
+
+ AddStep("click at centre of playfield", () =>
+ {
+ var hitObjectContainer = Editor.ChildrenOfType().Single();
+ var centre = hitObjectContainer.ToScreenSpace(OsuPlayfield.BASE_SIZE / 2);
+ InputManager.MoveMouseTo(centre);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("frontmost object selected", () =>
+ {
+ var hasCombo = Editor.ChildrenOfType().Single(b => b.IsSelected).Item as IHasComboInformation;
+ return hasCombo?.IndexInCurrentCombo == 0;
+ });
+ }
+
+ [Test]
+ public void TestPlacementOfConcurrentObjectWithDuration()
+ {
+ AddStep("seek to timing point", () => EditorClock.Seek(2170));
+ AddStep("add hit circle", () => EditorBeatmap.Add(createHitCircle(2170, Vector2.Zero)));
+
+ AddStep("choose spinner placement tool", () =>
+ {
+ InputManager.Key(Key.Number4);
+ var hitObjectContainer = Editor.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(hitObjectContainer.ScreenSpaceDrawQuad.Centre);
+ });
+
+ AddStep("begin placing spinner", () =>
+ {
+ InputManager.Click(MouseButton.Left);
+ });
+ AddStep("end placing spinner", () =>
+ {
+ EditorClock.Seek(2500);
+ InputManager.Click(MouseButton.Right);
+ });
+
+ AddAssert("two timeline blueprints present", () => Editor.ChildrenOfType().Count() == 2);
+ }
+
+ private HitCircle createHitCircle(double startTime, Vector2 position) => new HitCircle
+ {
+ StartTime = startTime,
+ Position = position,
+ };
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs
deleted file mode 100644
index 976bf93c15..0000000000
--- a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Linq;
-using NUnit.Framework;
-using osu.Framework.Testing;
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Screens.Edit.Compose.Components;
-using osu.Game.Tests.Beatmaps;
-using osuTK;
-using osuTK.Input;
-
-namespace osu.Game.Tests.Visual.Editing
-{
- public class TestSceneBlueprintSelection : EditorTestScene
- {
- protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
-
- protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
-
- private EditorBlueprintContainer blueprintContainer
- => Editor.ChildrenOfType().First();
-
- [Test]
- public void TestSelectedObjectHasPriorityWhenOverlapping()
- {
- var firstSlider = new Slider
- {
- Path = new SliderPath(new[]
- {
- new PathControlPoint(new Vector2()),
- new PathControlPoint(new Vector2(150, -50)),
- new PathControlPoint(new Vector2(300, 0))
- }),
- Position = new Vector2(0, 100)
- };
- var secondSlider = new Slider
- {
- Path = new SliderPath(new[]
- {
- new PathControlPoint(new Vector2()),
- new PathControlPoint(new Vector2(-50, 50)),
- new PathControlPoint(new Vector2(-100, 100))
- }),
- Position = new Vector2(200, 0)
- };
-
- AddStep("add overlapping sliders", () =>
- {
- EditorBeatmap.Add(firstSlider);
- EditorBeatmap.Add(secondSlider);
- });
- AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(firstSlider));
-
- AddStep("move mouse to common point", () =>
- {
- var pos = blueprintContainer.ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre;
- InputManager.MoveMouseTo(pos);
- });
- AddStep("right click", () => InputManager.Click(MouseButton.Right));
-
- AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider);
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs
similarity index 98%
rename from osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs
rename to osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs
index 4c4a87972f..a2a7b72283 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs
@@ -20,14 +20,14 @@ using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
- public class TestSceneEditorSelection : EditorTestScene
+ public class TestSceneComposerSelection : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
- private EditorBlueprintContainer blueprintContainer
- => Editor.ChildrenOfType().First();
+ private ComposeBlueprintContainer blueprintContainer
+ => Editor.ChildrenOfType().First();
private void moveMouseToObject(Func targetFunc)
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
new file mode 100644
index 0000000000..c81a1abfbc
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDifficultySwitching.cs
@@ -0,0 +1,177 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Overlays.Dialog;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Beatmaps.IO;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneDifficultySwitching : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override bool IsolateSavingFromDatabase => false;
+
+ [Resolved]
+ private OsuGameBase game { get; set; }
+
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
+
+ private BeatmapSetInfo importedBeatmapSet;
+
+ public override void SetUpSteps()
+ {
+ AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result);
+ base.SetUpSteps();
+ }
+
+ protected override void LoadEditor()
+ {
+ Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First());
+ base.LoadEditor();
+ }
+
+ [Test]
+ public void TestBasicSwitch()
+ {
+ BeatmapInfo targetDifficulty = null;
+
+ AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
+ switchToDifficulty(() => targetDifficulty);
+ confirmEditingBeatmap(() => targetDifficulty);
+
+ AddStep("exit editor", () => Stack.Exit());
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
+ [Test]
+ public void TestClockPositionPreservedBetweenSwitches()
+ {
+ BeatmapInfo targetDifficulty = null;
+ AddStep("seek editor to 00:05:00", () => EditorClock.Seek(5000));
+
+ AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
+ switchToDifficulty(() => targetDifficulty);
+ confirmEditingBeatmap(() => targetDifficulty);
+ AddAssert("editor clock at 00:05:00", () => EditorClock.CurrentTime == 5000);
+
+ AddStep("exit editor", () => Stack.Exit());
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
+ [Test]
+ public void TestClipboardPreservedAfterSwitch([Values] bool sameRuleset)
+ {
+ BeatmapInfo targetDifficulty = null;
+
+ AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
+ AddStep("copy object", () => Editor.Copy());
+
+ AddStep("set target difficulty", () =>
+ {
+ targetDifficulty = sameRuleset
+ ? importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.RulesetID == Beatmap.Value.BeatmapInfo.RulesetID)
+ : importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.RulesetID != Beatmap.Value.BeatmapInfo.RulesetID);
+ });
+ switchToDifficulty(() => targetDifficulty);
+ confirmEditingBeatmap(() => targetDifficulty);
+
+ AddAssert("no objects selected", () => !EditorBeatmap.SelectedHitObjects.Any());
+ AddStep("paste object", () => Editor.Paste());
+
+ if (sameRuleset)
+ AddAssert("object was pasted", () => EditorBeatmap.SelectedHitObjects.Any());
+ else
+ AddAssert("object was not pasted", () => !EditorBeatmap.SelectedHitObjects.Any());
+
+ AddStep("exit editor", () => Stack.Exit());
+
+ if (sameRuleset)
+ {
+ AddUntilStep("prompt for save dialog shown", () => DialogOverlay.CurrentDialog is PromptForSaveDialog);
+ AddStep("discard changes", () => ((PromptForSaveDialog)DialogOverlay.CurrentDialog).PerformOkAction());
+ }
+
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
+ [Test]
+ public void TestPreventSwitchDueToUnsavedChanges()
+ {
+ BeatmapInfo targetDifficulty = null;
+ PromptForSaveDialog saveDialog = null;
+
+ AddStep("remove first hitobject", () => EditorBeatmap.RemoveAt(0));
+
+ AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
+ switchToDifficulty(() => targetDifficulty);
+
+ AddUntilStep("prompt for save dialog shown", () =>
+ {
+ saveDialog = this.ChildrenOfType().Single();
+ return saveDialog != null;
+ });
+ AddStep("continue editing", () =>
+ {
+ var continueButton = saveDialog.ChildrenOfType().Last();
+ continueButton.TriggerClick();
+ });
+
+ confirmEditingBeatmap(() => importedBeatmapSet.Beatmaps.First());
+
+ AddRepeatStep("exit editor forcefully", () => Stack.Exit(), 2);
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
+ [Test]
+ public void TestAllowSwitchAfterDiscardingUnsavedChanges()
+ {
+ BeatmapInfo targetDifficulty = null;
+ PromptForSaveDialog saveDialog = null;
+
+ AddStep("remove first hitobject", () => EditorBeatmap.RemoveAt(0));
+
+ AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
+ switchToDifficulty(() => targetDifficulty);
+
+ AddUntilStep("prompt for save dialog shown", () =>
+ {
+ saveDialog = this.ChildrenOfType().Single();
+ return saveDialog != null;
+ });
+ AddStep("discard changes", () =>
+ {
+ var continueButton = saveDialog.ChildrenOfType().Single();
+ continueButton.TriggerClick();
+ });
+
+ confirmEditingBeatmap(() => targetDifficulty);
+
+ AddStep("exit editor forcefully", () => Stack.Exit());
+ // ensure editor loader didn't resume.
+ AddAssert("stack empty", () => Stack.CurrentScreen == null);
+ }
+
+ private void switchToDifficulty(Func difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
+
+ private void confirmEditingBeatmap(Func targetDifficulty)
+ {
+ AddUntilStep("current beatmap is correct", () => Beatmap.Value.BeatmapInfo.Equals(targetDifficulty.Invoke()));
+ AddUntilStep("current screen is editor", () => Stack.CurrentScreen == Editor && Editor?.IsLoaded == true);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index b6ae91844a..440d66ff9f 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Tests.Resources;
using SharpCompress.Archives;
@@ -55,6 +56,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestExitWithoutSave()
{
+ EditorBeatmap editorBeatmap = null;
+
+ AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
AddStep("exit without save", () =>
{
Editor.Exit();
@@ -62,7 +66,7 @@ namespace osu.Game.Tests.Visual.Editing
});
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
- AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
+ AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs
new file mode 100644
index 0000000000..85a98eca47
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs
@@ -0,0 +1,104 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneRectangularPositionSnapGrid : OsuManualInputManagerTestScene
+ {
+ private Container content;
+ protected override Container Content => content;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ base.Content.AddRange(new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.Gray
+ },
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
+ }
+
+ private static readonly object[][] test_cases =
+ {
+ new object[] { new Vector2(0, 0), new Vector2(10, 10) },
+ new object[] { new Vector2(240, 180), new Vector2(10, 15) },
+ new object[] { new Vector2(160, 120), new Vector2(30, 20) },
+ new object[] { new Vector2(480, 360), new Vector2(100, 100) },
+ };
+
+ [TestCaseSource(nameof(test_cases))]
+ public void TestRectangularGrid(Vector2 position, Vector2 spacing)
+ {
+ RectangularPositionSnapGrid grid = null;
+
+ AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid(position)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Spacing = spacing
+ });
+
+ AddStep("add snapping cursor", () => Add(new SnappingCursorContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos))
+ }));
+ }
+
+ private class SnappingCursorContainer : CompositeDrawable
+ {
+ public Func GetSnapPosition;
+
+ private readonly Drawable cursor;
+
+ public SnappingCursorContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = cursor = new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(50),
+ Colour = Color4.Red
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ updatePosition(GetContainingInputManager().CurrentState.Mouse.Position);
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ base.OnMouseMove(e);
+
+ updatePosition(e.ScreenSpaceMousePosition);
+ return true;
+ }
+
+ private void updatePosition(Vector2 screenSpacePosition)
+ {
+ cursor.Position = GetSnapPosition.Invoke(screenSpacePosition);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
new file mode 100644
index 0000000000..2544b6c2a1
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
@@ -0,0 +1,266 @@
+// 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 NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneTimelineSelection : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ private TimelineBlueprintContainer blueprintContainer
+ => Editor.ChildrenOfType().First();
+
+ private void moveMouseToObject(Func targetFunc)
+ {
+ AddStep("move mouse to object", () =>
+ {
+ var pos = blueprintContainer.SelectionBlueprints
+ .First(s => s.Item == targetFunc())
+ .ChildrenOfType()
+ .First().ScreenSpaceDrawQuad.Centre;
+
+ InputManager.MoveMouseTo(pos);
+ });
+ }
+
+ [Test]
+ public void TestNudgeSelection()
+ {
+ HitCircle[] addedObjects = null;
+
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
+ {
+ new HitCircle { StartTime = 100 },
+ new HitCircle { StartTime = 200, Position = new Vector2(100) },
+ new HitCircle { StartTime = 300, Position = new Vector2(200) },
+ new HitCircle { StartTime = 400, Position = new Vector2(300) },
+ }));
+
+ AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
+
+ AddStep("nudge forwards", () => InputManager.Key(Key.K));
+ AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 100);
+
+ AddStep("nudge backwards", () => InputManager.Key(Key.J));
+ AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
+ }
+
+ [Test]
+ public void TestBasicSelect()
+ {
+ var addedObject = new HitCircle { StartTime = 100 };
+ AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
+
+ moveMouseToObject(() => addedObject);
+ AddStep("left click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
+
+ var addedObject2 = new HitCircle
+ {
+ StartTime = 200,
+ Position = new Vector2(100),
+ };
+
+ AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2));
+ AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
+
+ moveMouseToObject(() => addedObject2);
+ AddStep("left click", () => InputManager.Click(MouseButton.Left));
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2);
+ }
+
+ [Test]
+ public void TestMultiSelect()
+ {
+ var addedObjects = new[]
+ {
+ new HitCircle { StartTime = 100 },
+ new HitCircle { StartTime = 200, Position = new Vector2(100) },
+ new HitCircle { StartTime = 300, Position = new Vector2(200) },
+ new HitCircle { StartTime = 400, Position = new Vector2(300) },
+ };
+
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
+
+ moveMouseToObject(() => addedObjects[0]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]);
+
+ AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
+
+ moveMouseToObject(() => addedObjects[1]);
+ AddStep("click second", () => InputManager.Click(MouseButton.Left));
+ AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
+
+ moveMouseToObject(() => addedObjects[2]);
+ AddStep("click third", () => InputManager.Click(MouseButton.Left));
+ AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2]));
+
+ moveMouseToObject(() => addedObjects[1]);
+ AddStep("click second", () => InputManager.Click(MouseButton.Left));
+ AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
+
+ AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
+ }
+
+ [Test]
+ public void TestBasicDeselect()
+ {
+ var addedObject = new HitCircle { StartTime = 100 };
+ AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
+
+ moveMouseToObject(() => addedObject);
+ AddStep("left click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
+
+ AddStep("click away", () =>
+ {
+ InputManager.MoveMouseTo(Editor.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("selection lost", () => EditorBeatmap.SelectedHitObjects.Count == 0);
+ }
+
+ [Test]
+ public void TestQuickDelete()
+ {
+ var addedObject = new HitCircle
+ {
+ StartTime = 0,
+ };
+
+ AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
+
+ moveMouseToObject(() => addedObject);
+
+ AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
+
+ AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
+ }
+
+ [Test]
+ public void TestRangeSelect()
+ {
+ var addedObjects = new[]
+ {
+ new HitCircle { StartTime = 100 },
+ new HitCircle { StartTime = 200, Position = new Vector2(100) },
+ new HitCircle { StartTime = 300, Position = new Vector2(200) },
+ new HitCircle { StartTime = 400, Position = new Vector2(300) },
+ new HitCircle { StartTime = 500, Position = new Vector2(400) },
+ };
+
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
+
+ moveMouseToObject(() => addedObjects[1]);
+ AddStep("click second", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[1]);
+
+ AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
+
+ moveMouseToObject(() => addedObjects[3]);
+ AddStep("click fourth", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects.Skip(1).Take(3));
+
+ moveMouseToObject(() => addedObjects[0]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects.Take(2));
+
+ AddStep("clear selection", () => EditorBeatmap.SelectedHitObjects.Clear());
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
+
+ moveMouseToObject(() => addedObjects[0]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects.Take(1));
+
+ AddStep("hold ctrl", () => InputManager.PressKey(Key.ControlLeft));
+ moveMouseToObject(() => addedObjects[2]);
+ AddStep("click third", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(new[] { addedObjects[0], addedObjects[2] });
+
+ AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
+ moveMouseToObject(() => addedObjects[4]);
+ AddStep("click fifth", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects.Except(new[] { addedObjects[1] }));
+
+ moveMouseToObject(() => addedObjects[0]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects);
+
+ AddStep("clear selection", () => EditorBeatmap.SelectedHitObjects.Clear());
+ moveMouseToObject(() => addedObjects[0]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects.Take(1));
+
+ moveMouseToObject(() => addedObjects[1]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects.Take(2));
+
+ moveMouseToObject(() => addedObjects[2]);
+ AddStep("click first", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects.Take(3));
+
+ AddStep("release keys", () =>
+ {
+ InputManager.ReleaseKey(Key.ControlLeft);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ });
+ }
+
+ [Test]
+ public void TestRangeSelectAfterExternalSelection()
+ {
+ var addedObjects = new[]
+ {
+ new HitCircle { StartTime = 100 },
+ new HitCircle { StartTime = 200, Position = new Vector2(100) },
+ new HitCircle { StartTime = 300, Position = new Vector2(200) },
+ new HitCircle { StartTime = 400, Position = new Vector2(300) },
+ };
+
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
+
+ AddStep("select all without mouse", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
+ assertSelectionIs(addedObjects);
+
+ AddStep("hold down shift", () => InputManager.PressKey(Key.ShiftLeft));
+
+ moveMouseToObject(() => addedObjects[1]);
+ AddStep("click second object", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects);
+
+ moveMouseToObject(() => addedObjects[3]);
+ AddStep("click fourth object", () => InputManager.Click(MouseButton.Left));
+ assertSelectionIs(addedObjects.Skip(1));
+
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
+ }
+
+ private void assertSelectionIs(IEnumerable hitObjects)
+ => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects));
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
index b7dcad3825..00b5c38e20 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Configuration;
@@ -71,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var working = CreateWorkingBeatmap(rulesetInfo);
Beatmap.Value = working;
- SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) };
+ SelectedMods.Value = new[] { ruleset.CreateMod() };
Player = CreatePlayer(ruleset);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index 7accaef818..1ba0965ceb 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
@@ -137,6 +138,23 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("no circle added", () => !this.ChildrenOfType().Any());
}
+ [Test]
+ public void TestClear()
+ {
+ AddStep("OD 1", () => recreateDisplay(new OsuHitWindows(), 1));
+
+ AddStep("hit", () => newJudgement(0.2D));
+ AddAssert("bar added", () => this.ChildrenOfType().All(
+ meter => meter.ChildrenOfType().Count() == 1));
+ AddAssert("circle added", () => this.ChildrenOfType().All(
+ meter => meter.ChildrenOfType().Count() == 1));
+
+ AddStep("clear", () => this.ChildrenOfType().ForEach(meter => meter.Clear()));
+
+ AddAssert("bar cleared", () => !this.ChildrenOfType().Any());
+ AddAssert("colour cleared", () => !this.ChildrenOfType().Any());
+ }
+
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
{
hitWindows?.SetDifficulty(overallDifficulty);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs
index 6de85499c5..0a39d94027 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
@@ -80,13 +81,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public bool ReceivedAction;
- public bool OnPressed(TestAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
- ReceivedAction = action == TestAction.Down;
+ ReceivedAction = e.Action == TestAction.Down;
return true;
}
- public void OnReleased(TestAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs
new file mode 100644
index 0000000000..ce5cd629e0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs
@@ -0,0 +1,128 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [TestFixture]
+ public class TestSceneParticleSpewer : OsuTestScene
+ {
+ private TestParticleSpewer spewer;
+
+ [Resolved]
+ private SkinManager skinManager { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Child = spewer = createSpewer();
+
+ AddToggleStep("toggle spawning", value => spewer.Active.Value = value);
+ AddSliderStep("particle gravity", 0f, 1f, 0f, value => spewer.Gravity = value);
+ AddSliderStep("particle velocity", 0f, 1f, 0.5f, value => spewer.MaxVelocity = value);
+ AddStep("move to new location", () =>
+ {
+ spewer.TransformTo(nameof(spewer.SpawnPosition), new Vector2(RNG.NextSingle(), RNG.NextSingle()), 1000, Easing.Out);
+ });
+ }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create spewer", () => Child = spewer = createSpewer());
+ }
+
+ [Test]
+ public void TestPresence()
+ {
+ AddStep("start spewer", () => spewer.Active.Value = true);
+ AddAssert("is present", () => spewer.IsPresent);
+
+ AddWaitStep("wait for some particles", 3);
+ AddStep("stop spewer", () => spewer.Active.Value = false);
+
+ AddWaitStep("wait for clean screen", 8);
+ AddAssert("is not present", () => !spewer.IsPresent);
+ }
+
+ [Test]
+ public void TestTimeJumps()
+ {
+ ManualClock testClock = new ManualClock();
+
+ AddStep("prepare clock", () =>
+ {
+ testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -3;
+ spewer.Clock = new FramedClock(testClock);
+ });
+ AddStep("start spewer", () => spewer.Active.Value = true);
+ AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1);
+
+ AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3);
+ AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2);
+
+ AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1);
+ AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3);
+ }
+
+ private TestParticleSpewer createSpewer() =>
+ new TestParticleSpewer(skinManager.DefaultLegacySkin.GetTexture("star2"))
+ {
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.Both,
+ RelativeSizeAxes = Axes.Both,
+ Position = new Vector2(0.5f),
+ Size = new Vector2(0.5f),
+ };
+
+ private class TestParticleSpewer : ParticleSpewer
+ {
+ public const int MAX_DURATION = 1500;
+ private const int rate = 250;
+
+ public int TotalCreatedParticles { get; private set; }
+
+ public float Gravity;
+
+ public float MaxVelocity = 0.25f;
+
+ public Vector2 SpawnPosition { get; set; } = new Vector2(0.5f);
+
+ protected override float ParticleGravity => Gravity;
+
+ public TestParticleSpewer(Texture texture)
+ : base(texture, rate, MAX_DURATION)
+ {
+ }
+
+ protected override FallingParticle CreateParticle()
+ {
+ TotalCreatedParticles++;
+
+ return new FallingParticle
+ {
+ Velocity = new Vector2(
+ RNG.NextSingle(-MaxVelocity, MaxVelocity),
+ RNG.NextSingle(-MaxVelocity, MaxVelocity)
+ ),
+ StartPosition = SpawnPosition,
+ Duration = RNG.NextSingle(MAX_DURATION),
+ StartAngle = RNG.NextSingle(MathF.PI * 2),
+ EndAngle = RNG.NextSingle(MathF.PI * 2),
+ EndScale = RNG.NextSingle(0.5f, 1.5f)
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index b38f7a998d..0a3fedaf8e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -54,7 +54,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
- Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay })
+ Recorder = recorder = new TestReplayRecorder(new Score
+ {
+ Replay = replay,
+ ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo }
+ })
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
@@ -222,13 +226,13 @@ namespace osu.Game.Tests.Visual.Gameplay
return base.OnMouseMove(e);
}
- public bool OnPressed(TestAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
box.Colour = Color4.White;
return true;
}
- public void OnReleased(TestAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
box.Colour = Color4.Black;
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
index 6e338b7202..dfd5e2dc58 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
@@ -45,7 +45,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
- Recorder = new TestReplayRecorder(new Score { Replay = replay })
+ Recorder = new TestReplayRecorder(new Score
+ {
+ Replay = replay,
+ ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo }
+ })
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos)
},
@@ -155,13 +159,13 @@ namespace osu.Game.Tests.Visual.Gameplay
return base.OnMouseMove(e);
}
- public bool OnPressed(TestAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
box.Colour = Color4.White;
return true;
}
- public void OnReleased(TestAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
box.Colour = Color4.Black;
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
index bb577886cc..6f5f774758 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs
@@ -279,13 +279,13 @@ namespace osu.Game.Tests.Visual.Gameplay
return base.OnMouseMove(e);
}
- public bool OnPressed(TestAction action)
+ public bool OnPressed(KeyBindingPressEvent e)
{
box.Colour = Color4.White;
return true;
}
- public void OnReleased(TestAction action)
+ public void OnReleased(KeyBindingReleaseEvent e)
{
box.Colour = Color4.Black;
}
@@ -354,7 +354,7 @@ namespace osu.Game.Tests.Visual.Gameplay
internal class TestReplayRecorder : ReplayRecorder
{
public TestReplayRecorder()
- : base(new Score())
+ : base(new Score { ScoreInfo = { Beatmap = new BeatmapInfo() } })
{
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs
new file mode 100644
index 0000000000..5fdadfc2fb
--- /dev/null
+++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs
@@ -0,0 +1,41 @@
+// 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.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Login;
+
+namespace osu.Game.Tests.Visual.Menus
+{
+ [TestFixture]
+ public class TestSceneLoginPanel : OsuManualInputManagerTestScene
+ {
+ private LoginPanel loginPanel;
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create login dialog", () =>
+ {
+ Add(loginPanel = new LoginPanel
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 0.5f,
+ });
+ });
+ }
+
+ [Test]
+ public void TestBasicLogin()
+ {
+ AddStep("logout", () => API.Logout());
+
+ AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password");
+ AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick());
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
index 3973dc57b2..b1f5781f6f 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs
@@ -130,6 +130,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Type = { Value = MatchType.HeadToHead },
}));
+ AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any());
+
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha));
AddStep("set password", () => room.Password.Value = "password");
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
index 99f6ab1ae1..b7da31a2b5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerLoungeSubScreen.cs
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -51,6 +52,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room join password correct", () => lastJoinedPassword == null);
}
+ [Test]
+ public void TestPopoverHidesOnBackButton()
+ {
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ AddStep("select room", () => InputManager.Key(Key.Down));
+ AddStep("attempt join room", () => InputManager.Key(Key.Enter));
+
+ AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType().Any());
+
+ AddAssert("textbox has focus", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
+
+ AddStep("hit escape", () => InputManager.Key(Key.Escape));
+ AddAssert("textbox lost focus", () => InputManager.FocusedDrawable is SearchTextBox);
+
+ AddStep("hit escape", () => InputManager.Key(Key.Escape));
+ AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType().Any());
+ }
+
[Test]
public void TestPopoverHidesOnLeavingScreen()
{
@@ -64,7 +83,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
- public void TestJoinRoomWithPassword()
+ public void TestJoinRoomWithIncorrectPasswordViaButton()
+ {
+ DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
+
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ AddStep("select room", () => InputManager.Key(Key.Down));
+ AddStep("attempt join room", () => InputManager.Key(Key.Enter));
+ AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
+ AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong");
+ AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType().First().TriggerClick());
+
+ AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
+ AddUntilStep("password prompt still visible", () => passwordEntryPopover.State.Value == Visibility.Visible);
+ AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
+ }
+
+ [Test]
+ public void TestJoinRoomWithIncorrectPasswordViaEnter()
+ {
+ DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
+
+ AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
+ AddStep("select room", () => InputManager.Key(Key.Down));
+ AddStep("attempt join room", () => InputManager.Key(Key.Enter));
+ AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType().FirstOrDefault()) != null);
+ AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType().First().Text = "wrong");
+ AddStep("press enter", () => InputManager.Key(Key.Enter));
+
+ AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
+ AddUntilStep("password prompt still visible", () => passwordEntryPopover.State.Value == Visibility.Visible);
+ AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
+ }
+
+ [Test]
+ public void TestJoinRoomWithCorrectPassword()
{
DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
index 4e08ffef17..44a8d7b439 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@@ -15,11 +16,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
SelectedRoom.Value = new Room();
- Child = new MultiplayerMatchFooter
+ Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Height = 50
+ RelativeSizeAxes = Axes.X,
+ Height = 50,
+ Child = new MultiplayerMatchFooter()
};
});
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index b8232837b5..e2baa82ba0 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -75,7 +75,6 @@ namespace osu.Game.Tests.Visual.Navigation
typeof(FileStore),
typeof(ScoreManager),
typeof(BeatmapManager),
- typeof(SettingsStore),
typeof(RulesetConfigCache),
typeof(OsuColour),
typeof(IBindable),
@@ -97,9 +96,6 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddStep("create game", () =>
{
- game = new OsuGame();
- game.SetHost(host);
-
Children = new Drawable[]
{
new Box
@@ -107,8 +103,9 @@ namespace osu.Game.Tests.Visual.Navigation
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
- game
};
+
+ AddGame(game = new OsuGame());
});
AddUntilStep("wait for load", () => game.IsLoaded);
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index b536233ff0..aeb800f58a 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -15,6 +15,7 @@ using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play;
@@ -76,7 +77,13 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press enter", () => InputManager.Key(Key.Enter));
- AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
+ AddUntilStep("wait for player", () =>
+ {
+ // dismiss any notifications that may appear (ie. muted notification).
+ clickMouseInCentre();
+ return (player = Game.ScreenStack.CurrentScreen as Player) != null;
+ });
+
AddAssert("retry count is 0", () => player.RestartCount == 0);
AddStep("attempt to retry", () => player.ChildrenOfType().First().Action());
@@ -103,7 +110,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } });
AddStep("press enter", () => InputManager.Key(Key.Enter));
- AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
+
+ AddUntilStep("wait for player", () =>
+ {
+ // dismiss any notifications that may appear (ie. muted notification).
+ clickMouseInCentre();
+ return (player = Game.ScreenStack.CurrentScreen as Player) != null;
+ });
+
AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning);
AddStep("seek to near end", () => player.ChildrenOfType().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000));
AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded);
@@ -130,7 +144,13 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press enter", () => InputManager.Key(Key.Enter));
- AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
+ AddUntilStep("wait for player", () =>
+ {
+ // dismiss any notifications that may appear (ie. muted notification).
+ clickMouseInCentre();
+ return (player = Game.ScreenStack.CurrentScreen as Player) != null;
+ });
+
AddUntilStep("wait for fail", () => player.HasFailed);
AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
@@ -388,6 +408,27 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden);
}
+ [Test]
+ public void TestExitGameFromSongSelect()
+ {
+ PushAndConfirm(() => new TestPlaySongSelect());
+ exitViaEscapeAndConfirm();
+
+ pushEscape(); // returns to osu! logo
+
+ AddStep("Hold escape", () => InputManager.PressKey(Key.Escape));
+ AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroTriangles);
+ AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape));
+ AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null);
+ AddStep("test dispose doesn't crash", () => Game.Dispose());
+ }
+
+ private void clickMouseInCentre()
+ {
+ InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
+ InputManager.Click(MouseButton.Left);
+ }
+
private void pushEscape() =>
AddStep("Press escape", () => InputManager.Key(Key.Escape));
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index 5bfb676f81..963809ebe1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -5,10 +5,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -17,10 +17,11 @@ using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
- public class TestSceneBeatmapListingOverlay : OsuTestScene
+ public class TestSceneBeatmapListingOverlay : OsuManualInputManagerTestScene
{
private readonly List setsForResponse = new List();
@@ -28,27 +29,33 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType().Single();
- [BackgroundDependencyLoader]
- private void load()
+ [SetUpSteps]
+ public void SetUpSteps()
{
- Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } };
-
- ((DummyAPIAccess)API).HandleRequest = req =>
+ AddStep("setup overlay", () =>
{
- if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false;
-
- searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
- {
- BeatmapSets = setsForResponse,
- });
-
- return true;
- };
+ Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } };
+ setsForResponse.Clear();
+ });
AddStep("initialize dummy", () =>
{
+ var api = (DummyAPIAccess)API;
+
+ api.HandleRequest = req =>
+ {
+ if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false;
+
+ searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
+ {
+ BeatmapSets = setsForResponse,
+ });
+
+ return true;
+ };
+
// non-supporter user
- ((DummyAPIAccess)API).LocalUser.Value = new User
+ api.LocalUser.Value = new User
{
Username = "TestBot",
Id = API.LocalUser.Value.Id + 1,
@@ -56,6 +63,51 @@ namespace osu.Game.Tests.Visual.Online
});
}
+ [Test]
+ public void TestHideViaBack()
+ {
+ AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
+ AddStep("hide", () => InputManager.Key(Key.Escape));
+ AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden);
+ }
+
+ [Test]
+ public void TestHideViaBackWithSearch()
+ {
+ AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
+
+ AddStep("search something", () => overlay.ChildrenOfType().First().Text = "search");
+
+ AddStep("kill search", () => InputManager.Key(Key.Escape));
+
+ AddAssert("search textbox empty", () => string.IsNullOrEmpty(overlay.ChildrenOfType().First().Text));
+ AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
+
+ AddStep("hide", () => InputManager.Key(Key.Escape));
+ AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden);
+ }
+
+ [Test]
+ public void TestHideViaBackWithScrolledSearch()
+ {
+ AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
+
+ AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, 100).ToArray()));
+
+ AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any(d => d.IsPresent));
+
+ AddStep("scroll to bottom", () => overlay.ChildrenOfType().First().ScrollToEnd());
+
+ AddStep("kill search", () => InputManager.Key(Key.Escape));
+
+ AddUntilStep("search textbox empty", () => string.IsNullOrEmpty(overlay.ChildrenOfType().First().Text));
+ AddUntilStep("is scrolled to top", () => overlay.ChildrenOfType().First().Current == 0);
+ AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
+
+ AddStep("hide", () => InputManager.Key(Key.Escape));
+ AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden);
+ }
+
[Test]
public void TestNoBeatmapsPlaceholder()
{
@@ -63,7 +115,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
- AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any());
+ AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any(d => d.IsPresent));
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
@@ -193,13 +245,15 @@ namespace osu.Game.Tests.Visual.Online
noPlaceholderShown();
}
+ private static int searchCount;
+
private void fetchFor(params BeatmapSetInfo[] beatmaps)
{
setsForResponse.Clear();
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
// trigger arbitrary change for fetching.
- searchControl.Query.TriggerChange();
+ searchControl.Query.Value = $"search {searchCount++}";
}
private void setRankAchievedFilter(ScoreRank[] ranks)
@@ -229,8 +283,8 @@ namespace osu.Game.Tests.Visual.Online
private void noPlaceholderShown()
{
AddUntilStep("no placeholder shown", () =>
- !overlay.ChildrenOfType().Any()
- && !overlay.ChildrenOfType().Any());
+ !overlay.ChildrenOfType().Any(d => d.IsPresent)
+ && !overlay.ChildrenOfType().Any(d => d.IsPresent));
}
private class TestAPIBeatmapSet : APIBeatmapSet
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index edc1696456..f420ad976b 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Tests.Visual.Online
protected override bool UseOnlineAPI => true;
+ private int nextBeatmapSetId = 1;
+
public TestSceneBeatmapSetOverlay()
{
Add(overlay = new TestBeatmapSetOverlay());
@@ -240,12 +242,23 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("show explicit map", () =>
{
- var beatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
+ var beatmapSet = getBeatmapSet();
beatmapSet.OnlineInfo.HasExplicitContent = true;
overlay.ShowBeatmapSet(beatmapSet);
});
}
+ [Test]
+ public void TestFeaturedBeatmap()
+ {
+ AddStep("show featured map", () =>
+ {
+ var beatmapSet = getBeatmapSet();
+ beatmapSet.OnlineInfo.TrackId = 1;
+ overlay.ShowBeatmapSet(beatmapSet);
+ });
+ }
+
[Test]
public void TestHide()
{
@@ -308,6 +321,14 @@ namespace osu.Game.Tests.Visual.Online
};
}
+ private BeatmapSetInfo getBeatmapSet()
+ {
+ var beatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
+ // Make sure the overlay is reloaded (see `BeatmapSetInfo.Equals`).
+ beatmapSet.OnlineBeatmapSetID = nextBeatmapSetId++;
+ return beatmapSet;
+ }
+
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/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 7cfca31167..609e637914 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.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 System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -85,6 +86,22 @@ namespace osu.Game.Tests.Visual.Online
case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess();
return true;
+
+ case GetUserRequest getUser:
+ if (getUser.Lookup.Equals("some body", StringComparison.OrdinalIgnoreCase))
+ {
+ getUser.TriggerSuccess(new User
+ {
+ Username = "some body",
+ Id = 1,
+ });
+ }
+ else
+ {
+ getUser.TriggerFailure(new Exception());
+ }
+
+ return true;
}
return false;
@@ -322,6 +339,27 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Current channel is channel 1", () => currentChannel == channel1);
}
+ [Test]
+ public void TestChatCommand()
+ {
+ AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
+ AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
+
+ AddStep("Open chat with user", () => channelManager.PostCommand("chat some body"));
+ AddAssert("PM channel is selected", () =>
+ channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body");
+
+ AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat nobody"));
+ AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage);
+
+ // Make sure no unnecessary requests are made when the PM channel is already open.
+ AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
+ AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null);
+ AddStep("Open chat with user", () => channelManager.PostCommand("chat some body"));
+ AddAssert("PM channel is selected", () =>
+ channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body");
+ }
+
private void pressChannelHotkey(int number)
{
var channelKey = Key.Number0 + number;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
index fd5f306e07..722010ace2 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
@@ -99,16 +99,23 @@ namespace osu.Game.Tests.Visual.Online
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- var normal = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
+ var normal = getBeatmapSet();
normal.OnlineInfo.HasVideo = true;
normal.OnlineInfo.HasStoryboard = true;
var undownloadable = getUndownloadableBeatmapSet();
var manyDifficulties = getManyDifficultiesBeatmapSet(rulesets);
- var explicitMap = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
+ var explicitMap = getBeatmapSet();
explicitMap.OnlineInfo.HasExplicitContent = true;
+ var featuredMap = getBeatmapSet();
+ featuredMap.OnlineInfo.TrackId = 1;
+
+ var explicitFeaturedMap = getBeatmapSet();
+ explicitFeaturedMap.OnlineInfo.HasExplicitContent = true;
+ explicitFeaturedMap.OnlineInfo.TrackId = 2;
+
Child = new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
@@ -125,13 +132,19 @@ namespace osu.Game.Tests.Visual.Online
new GridBeatmapPanel(undownloadable),
new GridBeatmapPanel(manyDifficulties),
new GridBeatmapPanel(explicitMap),
+ new GridBeatmapPanel(featuredMap),
+ new GridBeatmapPanel(explicitFeaturedMap),
new ListBeatmapPanel(normal),
new ListBeatmapPanel(undownloadable),
new ListBeatmapPanel(manyDifficulties),
- new ListBeatmapPanel(explicitMap)
+ new ListBeatmapPanel(explicitMap),
+ new ListBeatmapPanel(featuredMap),
+ new ListBeatmapPanel(explicitFeaturedMap)
},
},
};
+
+ BeatmapSetInfo getBeatmapSet() => CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
}
}
}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
index dafa8300f6..5c248163d7 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs
@@ -32,6 +32,12 @@ namespace osu.Game.Tests.Visual.Playlists
private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First();
+ [Test]
+ public void TestManyRooms()
+ {
+ AddStep("add rooms", () => RoomManager.AddRooms(500));
+ }
+
[Test]
public void TestScrollByDraggingRooms()
{
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index 61d49e4018..4bcc887b9f 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -160,11 +160,14 @@ namespace osu.Game.Tests.Visual.Playlists
Ruleset = { Value = new OsuRuleset().RulesetInfo }
}));
});
+
+ AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
}
private void waitForDisplay()
{
AddUntilStep("wait for request to complete", () => requestComplete);
+ AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
AddWaitStep("wait for display", 5);
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index ba6b6bd529..631455b727 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
- AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
{
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
- AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
{
@@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
- AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
ScorePanel expandedPanel = null;
ScorePanel contractedPanel = null;
@@ -223,6 +223,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ AddUntilStep("wait for load", () => this.ChildrenOfType().Single().AllPanelsVisible);
AddAssert("download button is disabled", () => !screen.ChildrenOfType().Last().Enabled.Value);
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
index e65dcb19b1..6f3b3028be 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
@@ -159,6 +159,9 @@ namespace osu.Game.Tests.Visual.Ranking
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ firstScore.User.Username = "A";
+ secondScore.User.Username = "B";
+
createListStep(() => new ScorePanelList());
AddStep("add scores and select first", () =>
@@ -168,6 +171,8 @@ namespace osu.Game.Tests.Visual.Ranking
list.SelectedScore.Value = firstScore;
});
+ AddUntilStep("wait for load", () => list.AllPanelsVisible);
+
assertScoreState(firstScore, true);
assertScoreState(secondScore, false);
@@ -182,6 +187,87 @@ namespace osu.Game.Tests.Visual.Ranking
assertExpandedPanelCentred();
}
+ [Test]
+ public void TestAddScoreImmediately()
+ {
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+
+ createListStep(() =>
+ {
+ var newList = new ScorePanelList { SelectedScore = { Value = score } };
+ newList.AddScore(score);
+ return newList;
+ });
+
+ assertScoreState(score, true);
+ assertExpandedPanelCentred();
+ }
+
+ [Test]
+ public void TestKeyboardNavigation()
+ {
+ var lowestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 100 };
+ var middleScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 200 };
+ var highestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 300 };
+
+ createListStep(() => new ScorePanelList());
+
+ AddStep("add scores and select middle", () =>
+ {
+ // order of addition purposefully scrambled.
+ list.AddScore(middleScore);
+ list.AddScore(lowestScore);
+ list.AddScore(highestScore);
+ list.SelectedScore.Value = middleScore;
+ });
+
+ assertScoreState(highestScore, false);
+ assertScoreState(middleScore, true);
+ assertScoreState(lowestScore, false);
+
+ AddStep("press left", () => InputManager.Key(Key.Left));
+
+ assertScoreState(highestScore, true);
+ assertScoreState(middleScore, false);
+ assertScoreState(lowestScore, false);
+ assertExpandedPanelCentred();
+
+ AddStep("press left at start of list", () => InputManager.Key(Key.Left));
+
+ assertScoreState(highestScore, true);
+ assertScoreState(middleScore, false);
+ assertScoreState(lowestScore, false);
+ assertExpandedPanelCentred();
+
+ AddStep("press right", () => InputManager.Key(Key.Right));
+
+ assertScoreState(highestScore, false);
+ assertScoreState(middleScore, true);
+ assertScoreState(lowestScore, false);
+ assertExpandedPanelCentred();
+
+ AddStep("press right again", () => InputManager.Key(Key.Right));
+
+ assertScoreState(highestScore, false);
+ assertScoreState(middleScore, false);
+ assertScoreState(lowestScore, true);
+ assertExpandedPanelCentred();
+
+ AddStep("press right at end of list", () => InputManager.Key(Key.Right));
+
+ assertScoreState(highestScore, false);
+ assertScoreState(middleScore, false);
+ assertScoreState(lowestScore, true);
+ assertExpandedPanelCentred();
+
+ AddStep("press left", () => InputManager.Key(Key.Left));
+
+ assertScoreState(highestScore, false);
+ assertScoreState(middleScore, true);
+ assertScoreState(lowestScore, false);
+ assertExpandedPanelCentred();
+ }
+
private void createListStep(Func creationFunc)
{
AddStep("create list", () => Child = list = creationFunc().With(d =>
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
index 57ba051214..168d9fafcf 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
@@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Settings
{
AddAssert($"Check {name} is bound to {keyName}", () =>
{
- var firstRow = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text == name));
+ var firstRow = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == name));
var firstButton = firstRow.ChildrenOfType().First();
return firstButton.Text.Text == keyName;
@@ -247,7 +247,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep($"Scroll to {name}", () =>
{
- var firstRow = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text == name));
+ var firstRow = panel.ChildrenOfType().First(r => r.ChildrenOfType().Any(s => s.Text.ToString() == name));
firstButton = firstRow.ChildrenOfType().First();
panel.ChildrenOfType().First().ScrollTo(firstButton);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index da474a64ba..997eac709d 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -1,14 +1,15 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
-using osu.Framework.Platform;
+using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Overlays;
+using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osuTK;
@@ -17,22 +18,34 @@ namespace osu.Game.Tests.Visual.Settings
[TestFixture]
public class TestSceneTabletSettings : OsuTestScene
{
- [BackgroundDependencyLoader]
- private void load(GameHost host)
- {
- var tabletHandler = new TestTabletHandler();
+ private TestTabletHandler tabletHandler;
+ private TabletSettings settings;
- AddRange(new Drawable[]
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create settings", () =>
{
- new TabletSettings(tabletHandler)
+ tabletHandler = new TestTabletHandler();
+
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.None,
- Width = SettingsPanel.PANEL_WIDTH,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- }
+ settings = new TabletSettings(tabletHandler)
+ {
+ RelativeSizeAxes = Axes.None,
+ Width = SettingsPanel.PANEL_WIDTH,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ }
+ };
});
+ AddStep("set square size", () => tabletHandler.SetTabletSize(new Vector2(100, 100)));
+ }
+
+ [Test]
+ public void TestVariousTabletSizes()
+ {
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300)));
AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300)));
@@ -40,6 +53,71 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero));
}
+ [Test]
+ public void TestWideAspectRatioValidity()
+ {
+ AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
+
+ AddStep("Reset to full area", () => settings.ChildrenOfType().First().TriggerClick());
+ ensureValid();
+
+ AddStep("rotate 10", () => tabletHandler.Rotation.Value = 10);
+ ensureInvalid();
+
+ AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
+ ensureInvalid();
+
+ AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
+ ensureInvalid();
+
+ AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
+ ensureValid();
+ }
+
+ [Test]
+ public void TestRotationValidity()
+ {
+ AddAssert("area valid", () => settings.AreaSelection.IsWithinBounds);
+
+ AddStep("rotate 90", () => tabletHandler.Rotation.Value = 90);
+ ensureValid();
+
+ AddStep("rotate 180", () => tabletHandler.Rotation.Value = 180);
+
+ ensureValid();
+
+ AddStep("rotate 270", () => tabletHandler.Rotation.Value = 270);
+
+ ensureValid();
+
+ AddStep("rotate 360", () => tabletHandler.Rotation.Value = 360);
+
+ ensureValid();
+
+ AddStep("rotate 0", () => tabletHandler.Rotation.Value = 0);
+ ensureValid();
+
+ AddStep("rotate 45", () => tabletHandler.Rotation.Value = 45);
+ ensureInvalid();
+
+ AddStep("rotate 0", () => tabletHandler.Rotation.Value = 0);
+ ensureValid();
+ }
+
+ [Test]
+ public void TestOffsetValidity()
+ {
+ ensureValid();
+ AddStep("move right", () => tabletHandler.AreaOffset.Value = Vector2.Zero);
+ ensureInvalid();
+ AddStep("move back", () => tabletHandler.AreaOffset.Value = tabletHandler.AreaSize.Value / 2);
+ ensureValid();
+ }
+
+ private void ensureValid() => AddAssert("area valid", () => settings.AreaSelection.IsWithinBounds);
+
+ private void ensureInvalid() => AddAssert("area invalid", () => !settings.AreaSelection.IsWithinBounds);
+
public class TestTabletHandler : ITabletHandler
{
public Bindable AreaOffset { get; } = new Bindable();
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
index 40b2f66d74..dcc2111ad3 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select EZ mod", () =>
{
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
- SelectedMods.Value = new[] { ruleset.GetAllMods().OfType().Single() };
+ SelectedMods.Value = new[] { ruleset.CreateMod() };
});
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
@@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select HR mod", () =>
{
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
- SelectedMods.Value = new[] { ruleset.GetAllMods().OfType().Single() };
+ SelectedMods.Value = new[] { ruleset.CreateMod() };
});
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
@@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select unchanged Difficulty Adjust mod", () =>
{
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
- var difficultyAdjustMod = ruleset.GetAllMods().OfType().Single();
+ var difficultyAdjustMod = ruleset.CreateMod