diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index b3f7c67c51..97fcb52ab1 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -27,7 +27,7 @@
]
},
"ppy.localisationanalyser.tools": {
- "version": "2021.608.0",
+ "version": "2021.705.0",
"commands": [
"localisation"
]
diff --git a/.editorconfig b/.editorconfig
index f4d7e08d08..19bd89c52f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -157,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning
#Style - variable declaration
csharp_style_inlined_variable_declaration = true:warning
-csharp_style_deconstructed_variable_declaration = true:warning
+csharp_style_deconstructed_variable_declaration = false:silent
#Style - other C# 7.x features
dotnet_style_prefer_inferred_tuple_names = true:warning
@@ -168,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
#Style - C# 8 features
csharp_prefer_static_local_function = true:warning
csharp_prefer_simple_using_statement = true:silent
-csharp_style_prefer_index_operator = true:warning
-csharp_style_prefer_range_operator = true:warning
+csharp_style_prefer_index_operator = false:silent
+csharp_style_prefer_range_operator = false:silent
csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ed3e99cb61..29cbdd2d37 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,6 +15,7 @@ jobs:
- { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
+ timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index 381d2d49c5..e0ccd50989 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -21,6 +21,7 @@ jobs:
- { prettyname: macOS }
- { prettyname: Linux }
threadingMode: ['SingleThread', 'MultiThreaded']
+ timeout-minutes: 5
steps:
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.4.2
diff --git a/.idea/.idea.osu.Desktop/.idea/dataSources.xml b/.idea/.idea.osu.Desktop/.idea/dataSources.xml
deleted file mode 100644
index 10f8c1c84d..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/dataSources.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- sqlite.xerial
- true
- org.sqlite.JDBC
- jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db
-
-
-
-
-
-
\ No newline at end of file
diff --git a/FodyWeavers.xml b/FodyWeavers.xml
index cc07b89533..ea490e3297 100644
--- a/FodyWeavers.xml
+++ b/FodyWeavers.xml
@@ -1,3 +1,3 @@
-
+
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 5eb5efa54c..3dd6be7307 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index d7c116411a..0c4bfe0ed7 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index 89b551286b..bb0a487274 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index d7c116411a..0c4bfe0ed7 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/osu.Android.props b/osu.Android.props
index 3c4380e355..9280eaf97c 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
-
-
+
+
-
+
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 47cd39dc5a..58d67c11d9 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -116,7 +116,7 @@ namespace osu.Desktop.Updater
if (scheduleRecheck)
{
// check again in 30 minutes.
- Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
+ Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
@@ -141,7 +141,7 @@ namespace osu.Desktop.Updater
Activated = () =>
{
updateManager.PrepareUpdateAsync()
- .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
+ .ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
return true;
};
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index ad5c323e9b..53a4e5edf5 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -5,8 +5,8 @@
true
A free-to-win rhythm game. Rhythm is just a *click* away!
osu!
-
osu!lazer
- osu!lazer
+ osu!
+ osu!
lazer.ico
app.manifest
0.0.0
diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec
index fa182f8e70..1757fd7c73 100644
--- a/osu.Desktop/osu.nuspec
+++ b/osu.Desktop/osu.nuspec
@@ -3,7 +3,7 @@
osulazer
0.0.0
- osu!lazer
+ osu!
ppy Pty Ltd
Dean Herbert
https://osu.ppy.sh/
@@ -20,4 +20,3 @@
-
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 7a74563b2b..da8a0540f4 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
new file mode 100644
index 0000000000..161c685043
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneEditor.cs
@@ -0,0 +1,14 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneEditor : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
new file mode 100644
index 0000000000..ec186bcfb2
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
@@ -0,0 +1,114 @@
+// 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.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Judgements;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using Direction = osu.Game.Rulesets.Catch.UI.Direction;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneCatchSkinConfiguration : OsuTestScene
+ {
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ private Catcher catcher;
+
+ private readonly Container container;
+
+ public TestSceneCatchSkinConfiguration()
+ {
+ Add(droppedObjectContainer = new DroppedObjectContainer());
+ Add(container = new Container { RelativeSizeAxes = Axes.Both });
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestCatcherPlateFlipping(bool flip)
+ {
+ AddStep("setup catcher", () =>
+ {
+ var skin = new TestSkin { FlipCatcherPlate = flip };
+ container.Child = new SkinProvidingContainer(skin)
+ {
+ Child = catcher = new Catcher(new Container())
+ {
+ Anchor = Anchor.Centre
+ }
+ };
+ });
+
+ Fruit fruit = new Fruit();
+
+ AddStep("catch fruit", () => catchFruit(fruit, 20));
+
+ float position = 0;
+
+ AddStep("record fruit position", () => position = getCaughtObjectPosition(fruit));
+
+ AddStep("face left", () => catcher.VisualDirection = Direction.Left);
+
+ if (flip)
+ AddAssert("fruit position changed", () => !Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+ else
+ AddAssert("fruit position unchanged", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+
+ AddStep("face right", () => catcher.VisualDirection = Direction.Right);
+
+ AddAssert("fruit position restored", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
+ }
+
+ private float getCaughtObjectPosition(Fruit fruit)
+ {
+ var caughtObject = catcher.ChildrenOfType().Single(c => c.HitObject == fruit);
+ return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
+ }
+
+ private void catchFruit(Fruit fruit, float x)
+ {
+ fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ var drawableFruit = new DrawableFruit(fruit) { X = x };
+ var judgement = fruit.CreateJudgement();
+ catcher.OnNewResult(drawableFruit, new CatchJudgementResult(fruit, judgement)
+ {
+ Type = judgement.MaxResult
+ });
+ }
+
+ private class TestSkin : DefaultSkin
+ {
+ public bool FlipCatcherPlate { get; set; }
+
+ public TestSkin()
+ : base(null)
+ {
+ }
+
+ public override IBindable GetConfig(TLookup lookup)
+ {
+ if (lookup is CatchSkinConfiguration config)
+ {
+ if (config == CatchSkinConfiguration.FlipCatcherPlate)
+ return SkinUtils.As(new Bindable(FlipCatcherPlate));
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 900691ecae..8359657f84 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -6,8 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
@@ -31,10 +31,23 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
- private Container droppedObjectContainer;
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ private readonly Container trailContainer;
private TestCatcher catcher;
+ public TestSceneCatcher()
+ {
+ Add(trailContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Depth = -1
+ });
+ Add(droppedObjectContainer = new DroppedObjectContainer());
+ }
+
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -43,20 +56,13 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0,
};
- var trailContainer = new Container();
- droppedObjectContainer = new Container();
- catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
+ if (catcher != null)
+ Remove(catcher);
- Child = new Container
+ Add(catcher = new TestCatcher(trailContainer, difficulty)
{
- Anchor = Anchor.Centre,
- Children = new Drawable[]
- {
- trailContainer,
- droppedObjectContainer,
- catcher
- }
- };
+ Anchor = Anchor.Centre
+ });
});
[Test]
@@ -188,9 +194,9 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
- catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
- catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
- catcher.CaughtObjects.Any(obj => obj.Y < -25));
+ catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
+ catcher.CaughtObjects.Any(obj => obj.Y < 0));
}
[Test]
@@ -293,8 +299,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable CaughtObjects => this.ChildrenOfType();
- public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty)
- : base(trailsTarget, droppedObjectTarget, difficulty)
+ public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty)
+ : base(trailsTarget, difficulty)
{
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index 4af5098451..877e115e2f 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -6,7 +6,6 @@ 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;
using osu.Framework.Threading;
using osu.Framework.Utils;
@@ -97,18 +96,12 @@ namespace osu.Game.Rulesets.Catch.Tests
SetContents(_ =>
{
- var droppedObjectContainer = new Container
- {
- RelativeSizeAxes = Axes.Both
- };
-
return new CatchInputManager(catchRuleset)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- droppedObjectContainer,
- new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
+ new TestCatcherArea(beatmapDifficulty)
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
@@ -126,9 +119,13 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
- public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
- : base(droppedObjectContainer, beatmapDifficulty)
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
+ : base(beatmapDifficulty)
{
+ AddInternal(droppedObjectContainer = new DroppedObjectContainer());
}
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
index 683a776dcc..e7b0259ea2 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -118,11 +118,10 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("create hyper-dashing catcher", () =>
{
- Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container())
+ Child = setupSkinHierarchy(catcherArea = new TestCatcherArea
{
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(4f),
+ Origin = Anchor.Centre
}, skin);
});
@@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("finish hyper-dashing", () =>
{
- catcherArea.MovableCatcher.SetHyperDashState(1);
+ catcherArea.MovableCatcher.SetHyperDashState();
catcherArea.MovableCatcher.FinishTransforms();
});
@@ -206,5 +205,18 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
}
+
+ private class TestCatcherArea : CatcherArea
+ {
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
+ public TestCatcherArea()
+ {
+ Scale = new Vector2(4f);
+
+ AddInternal(droppedObjectContainer = new DroppedObjectContainer());
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 83d0744588..484da8e22e 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index fac5d03833..3a5322ce82 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Beatmaps
@@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
public const int RNG_SEED = 1337;
+ public bool HardRockOffsets { get; set; }
+
public CatchBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
@@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
- public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
+ public void ApplyPositionOffsets(IBeatmap beatmap)
{
var rng = new FastRandom(RNG_SEED);
- bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
float? lastPosition = null;
double lastStartTime = 0;
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj)
{
case Fruit fruit:
- if (shouldApplyHardRockOffset)
+ if (HardRockOffsets)
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
break;
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 23ce444560..76863acc78 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
+using osu.Game.Rulesets.Edit;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@@ -175,12 +177,14 @@ namespace osu.Game.Rulesets.Catch
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
+
+ public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
new file mode 100644
index 0000000000..31075db7d1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class BananaShowerCompositionTool : HitObjectCompositionTool
+ {
+ public BananaShowerCompositionTool()
+ : base(nameof(BananaShower))
+ {
+ }
+
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
new file mode 100644
index 0000000000..6dea8b0712
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
@@ -0,0 +1,73 @@
+// 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.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint
+ {
+ private readonly TimeSpanOutline outline;
+
+ public BananaShowerPlacementBlueprint()
+ {
+ InternalChild = outline = new TimeSpanOutline();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ switch (PlacementActive)
+ {
+ case PlacementState.Waiting:
+ if (e.Button != MouseButton.Left) break;
+
+ BeginPlacement(true);
+ return true;
+
+ case PlacementState.Active:
+ if (e.Button != MouseButton.Right) break;
+
+ // If the duration is negative, swap the start and the end time to make the duration positive.
+ if (HitObject.Duration < 0)
+ {
+ HitObject.StartTime = HitObject.EndTime;
+ HitObject.Duration = -HitObject.Duration;
+ }
+
+ EndPlacement(HitObject.Duration > 0);
+ return true;
+ }
+
+ return base.OnMouseDown(e);
+ }
+
+ public override void UpdateTimeAndPosition(SnapResult result)
+ {
+ base.UpdateTimeAndPosition(result);
+
+ if (!(result.Time is double time)) return;
+
+ switch (PlacementActive)
+ {
+ case PlacementState.Waiting:
+ HitObject.StartTime = time;
+ break;
+
+ case PlacementState.Active:
+ HitObject.EndTime = time;
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
new file mode 100644
index 0000000000..9132b1a9e8
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerSelectionBlueprint.cs
@@ -0,0 +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 osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint
+ {
+ public BananaShowerSelectionBlueprint(BananaShower hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
new file mode 100644
index 0000000000..69054e2c81
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs
@@ -0,0 +1,27 @@
+// 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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class CatchPlacementBlueprint : PlacementBlueprint
+ where THitObject : CatchHitObject, new()
+ {
+ protected new THitObject HitObject => (THitObject)base.HitObject;
+
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ public CatchPlacementBlueprint()
+ : base(new THitObject())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
new file mode 100644
index 0000000000..298f9474b0
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchSelectionBlueprint.cs
@@ -0,0 +1,38 @@
+// 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.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public abstract class CatchSelectionBlueprint : HitObjectSelectionBlueprint
+ where THitObject : CatchHitObject
+ {
+ public override Vector2 ScreenSpaceSelectionPoint
+ {
+ get
+ {
+ float x = HitObject.OriginalX;
+ float y = HitObjectContainer.PositionAtTime(HitObject.StartTime);
+ return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
+ }
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos);
+
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ protected CatchSelectionBlueprint(THitObject hitObject)
+ : base(hitObject)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
new file mode 100644
index 0000000000..345b59bdcd
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/FruitOutline.cs
@@ -0,0 +1,38 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class FruitOutline : CompositeDrawable
+ {
+ public FruitOutline()
+ {
+ Anchor = Anchor.BottomLeft;
+ Origin = Anchor.Centre;
+ InternalChild = new BorderPiece();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ Colour = osuColour.Yellow;
+ }
+
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject, [CanBeNull] CatchHitObject parent = null)
+ {
+ X = hitObject.EffectiveX - (parent?.OriginalX ?? 0);
+ Y = hitObjectContainer.PositionAtTime(hitObject.StartTime, parent?.StartTime ?? hitObjectContainer.Time.Current);
+ Scale = new Vector2(hitObject.Scale);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.cs
new file mode 100644
index 0000000000..48d90e8b24
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/NestedOutlineContainer.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.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class NestedOutlineContainer : CompositeDrawable
+ {
+ private readonly List nestedHitObjects = new List();
+
+ public NestedOutlineContainer()
+ {
+ Anchor = Anchor.BottomLeft;
+ }
+
+ public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
+ {
+ X = parentHitObject.OriginalX;
+ Y = hitObjectContainer.PositionAtTime(parentHitObject.StartTime);
+ }
+
+ public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
+ {
+ nestedHitObjects.Clear();
+ nestedHitObjects.AddRange(parentHitObject.NestedHitObjects
+ .OfType()
+ .Where(h => !(h is TinyDroplet)));
+
+ while (nestedHitObjects.Count < InternalChildren.Count)
+ RemoveInternal(InternalChildren[^1]);
+
+ while (InternalChildren.Count < nestedHitObjects.Count)
+ AddInternal(new FruitOutline());
+
+ for (int i = 0; i < nestedHitObjects.Count; i++)
+ {
+ var hitObject = nestedHitObjects[i];
+ var outline = (FruitOutline)InternalChildren[i];
+ outline.UpdateFrom(hitObjectContainer, hitObject, parentHitObject);
+ outline.Scale *= hitObject is Droplet ? 0.5f : 1;
+ }
+ }
+
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
new file mode 100644
index 0000000000..96111beda4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/ScrollingPath.cs
@@ -0,0 +1,83 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Lines;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class ScrollingPath : CompositeDrawable
+ {
+ private readonly Path drawablePath;
+
+ private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
+
+ public ScrollingPath()
+ {
+ Anchor = Anchor.BottomLeft;
+
+ InternalChildren = new Drawable[]
+ {
+ drawablePath = new SmoothPath
+ {
+ PathRadius = 2,
+ Alpha = 0.5f
+ },
+ };
+ }
+
+ public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
+ {
+ X = hitObject.OriginalX;
+ Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ }
+
+ public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
+ {
+ double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
+
+ computeDistanceXs(hitObject);
+ drawablePath.Vertices = vertices
+ .Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
+ .ToArray();
+ drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
+ }
+
+ private void computeDistanceXs(JuiceStream hitObject)
+ {
+ vertices.Clear();
+
+ var sliderVertices = new List();
+ hitObject.Path.GetPathToProgress(sliderVertices, 0, 1);
+
+ if (sliderVertices.Count == 0)
+ return;
+
+ double distance = 0;
+ Vector2 lastPosition = Vector2.Zero;
+
+ for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
+ {
+ foreach (var position in sliderVertices)
+ {
+ distance += Vector2.Distance(lastPosition, position);
+ lastPosition = position;
+
+ vertices.Add((distance, position.X));
+ }
+
+ sliderVertices.Reverse();
+ }
+ }
+
+ // Because this has 0x0 size, the contents are otherwise masked away if the start position is outside the screen.
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
new file mode 100644
index 0000000000..65dfce0493
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/TimeSpanOutline.cs
@@ -0,0 +1,63 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
+{
+ public class TimeSpanOutline : CompositeDrawable
+ {
+ private const float border_width = 4;
+
+ private const float opacity_when_empty = 0.5f;
+
+ private bool isEmpty = true;
+
+ public TimeSpanOutline()
+ {
+ Anchor = Origin = Anchor.BottomLeft;
+ RelativeSizeAxes = Axes.X;
+
+ Masking = true;
+ BorderThickness = border_width;
+ Alpha = opacity_when_empty;
+
+ // A box is needed to make the border visible.
+ InternalChild = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Transparent
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour osuColour)
+ {
+ BorderColour = osuColour.Yellow;
+ }
+
+ public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject)
+ {
+ float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime);
+ float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime);
+
+ Y = Math.Max(startY, endY);
+ float height = Math.Abs(startY - endY);
+
+ bool wasEmpty = isEmpty;
+ isEmpty = height == 0;
+ if (wasEmpty != isEmpty)
+ this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150);
+
+ Height = Math.Max(height, border_width);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
new file mode 100644
index 0000000000..0f28cf6786
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitPlacementBlueprint.cs
@@ -0,0 +1,50 @@
+// 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.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class FruitPlacementBlueprint : CatchPlacementBlueprint
+ {
+ private readonly FruitOutline outline;
+
+ public FruitPlacementBlueprint()
+ {
+ InternalChild = outline = new FruitOutline();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ BeginPlacement();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button != MouseButton.Left) return base.OnMouseDown(e);
+
+ EndPlacement(true);
+ return true;
+ }
+
+ public override void UpdateTimeAndPosition(SnapResult result)
+ {
+ base.UpdateTimeAndPosition(result);
+
+ HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
new file mode 100644
index 0000000000..9665aac2fb
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/FruitSelectionBlueprint.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class FruitSelectionBlueprint : CatchSelectionBlueprint
+ {
+ private readonly FruitOutline outline;
+
+ public FruitSelectionBlueprint(Fruit hitObject)
+ : base(hitObject)
+ {
+ InternalChild = outline = new FruitOutline();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (IsSelected)
+ outline.UpdateFrom(HitObjectContainer, HitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
new file mode 100644
index 0000000000..bf7b962e0a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamSelectionBlueprint.cs
@@ -0,0 +1,92 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit.Blueprints
+{
+ public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint
+ {
+ public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
+
+ private float minNestedX;
+ private float maxNestedX;
+
+ private readonly ScrollingPath scrollingPath;
+
+ private readonly NestedOutlineContainer nestedOutlineContainer;
+
+ private readonly Cached pathCache = new Cached();
+
+ public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
+ : base(hitObject)
+ {
+ InternalChildren = new Drawable[]
+ {
+ scrollingPath = new ScrollingPath(),
+ nestedOutlineContainer = new NestedOutlineContainer()
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ HitObject.DefaultsApplied += onDefaultsApplied;
+ computeObjectBounds();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!IsSelected) return;
+
+ scrollingPath.UpdatePositionFrom(HitObjectContainer, HitObject);
+ nestedOutlineContainer.UpdatePositionFrom(HitObjectContainer, HitObject);
+
+ if (pathCache.IsValid) return;
+
+ scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
+ nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
+
+ pathCache.Validate();
+ }
+
+ private void onDefaultsApplied(HitObject _)
+ {
+ computeObjectBounds();
+ pathCache.Invalidate();
+ }
+
+ private void computeObjectBounds()
+ {
+ minNestedX = HitObject.NestedHitObjects.OfType().Min(nested => nested.OriginalX) - HitObject.OriginalX;
+ maxNestedX = HitObject.NestedHitObjects.OfType().Max(nested => nested.OriginalX) - HitObject.OriginalX;
+ }
+
+ private RectangleF getBoundingBox()
+ {
+ float left = HitObject.OriginalX + minNestedX;
+ float right = HitObject.OriginalX + maxNestedX;
+ float top = HitObjectContainer.PositionAtTime(HitObject.EndTime);
+ float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime);
+ float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale;
+ return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ HitObject.DefaultsApplied -= onDefaultsApplied;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
new file mode 100644
index 0000000000..7f2782a474
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchBlueprintContainer : ComposeBlueprintContainer
+ {
+ public CatchBlueprintContainer(CatchHitObjectComposer composer)
+ : base(composer)
+ {
+ }
+
+ protected override SelectionHandler CreateSelectionHandler() => new CatchSelectionHandler();
+
+ public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case Fruit fruit:
+ return new FruitSelectionBlueprint(fruit);
+
+ case JuiceStream juiceStream:
+ return new JuiceStreamSelectionBlueprint(juiceStream);
+
+ case BananaShower bananaShower:
+ return new BananaShowerSelectionBlueprint(bananaShower);
+ }
+
+ return base.CreateHitObjectBlueprintFor(hitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
new file mode 100644
index 0000000000..d383eb9ba6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchEditorPlayfield : CatchPlayfield
+ {
+ // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
+ public CatchEditorPlayfield(BeatmapDifficulty difficulty)
+ : base(difficulty)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // TODO: honor "hit animation" setting?
+ CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
+
+ // TODO: disable hit lighting as well
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
new file mode 100644
index 0000000000..d9712bc8e9
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchHitObjectComposer : HitObjectComposer
+ {
+ public CatchHitObjectComposer(CatchRuleset ruleset)
+ : base(ruleset)
+ {
+ }
+
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
+ new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
+
+ protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
+ {
+ new FruitCompositionTool(),
+ new BananaShowerCompositionTool()
+ };
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
+ // TODO: implement position snap
+ result.ScreenSpacePosition.X = screenSpacePosition.X;
+ return result;
+ }
+
+ protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
new file mode 100644
index 0000000000..d35d74d93d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchSelectionHandler.cs
@@ -0,0 +1,46 @@
+// 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 osu.Framework.Allocation;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchSelectionHandler : EditorSelectionHandler
+ {
+ protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ public override bool HandleMovement(MoveSelectionEvent moveEvent)
+ {
+ var blueprint = moveEvent.Blueprint;
+ Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
+ Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
+ float deltaX = targetPosition.X - originalPosition.X;
+
+ EditorBeatmap.PerformOnSelection(h =>
+ {
+ if (!(h is CatchHitObject hitObject)) return;
+
+ if (hitObject is BananaShower) return;
+
+ // TODO: confine in bounds
+ hitObject.OriginalX += deltaX;
+
+ // Move the nested hit objects to give an instant result before nested objects are recreated.
+ foreach (var nested in hitObject.NestedHitObjects.OfType())
+ nested.OriginalX += deltaX;
+ });
+
+ return true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
new file mode 100644
index 0000000000..0344709d45
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class DrawableCatchEditorRuleset : DrawableCatchRuleset
+ {
+ public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ : base(ruleset, beatmap, mods)
+ {
+ }
+
+ protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
new file mode 100644
index 0000000000..f776fe39c1
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit.Blueprints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class FruitCompositionTool : HitObjectCompositionTool
+ {
+ public FruitCompositionTool()
+ : base(nameof(Fruit))
+ {
+ }
+
+ public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
index 5f1736450a..bd7a1df2e4 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs
@@ -5,11 +5,12 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModDifficultyAdjust : ModDifficultyAdjust
+ public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension
@@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
+ [SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
+ public BindableBool HardRockOffsets { get; } = new BindableBool();
+
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
@@ -45,12 +49,14 @@ namespace osu.Game.Rulesets.Catch.Mods
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
+ string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
- approachRate
+ approachRate,
+ spicyPatterns,
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
@@ -70,5 +76,11 @@ namespace osu.Game.Rulesets.Catch.Mods
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
}
+
+ public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
+ {
+ var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
+ catchProcessor.HardRockOffsets = HardRockOffsets.Value;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
index 0dde6aa06e..68b6ce96a3 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
@@ -7,10 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
+ public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
{
public override double ScoreMultiplier => 1.12;
- public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
+ public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
+ {
+ var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
+ catchProcessor.HardRockOffsets = true;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index ae45182960..0b8c0e28a7 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.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 Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -20,6 +21,11 @@ namespace osu.Game.Rulesets.Catch.Objects
///
/// The horizontal position of the hit object between 0 and .
///
+ ///
+ /// Only setter is exposed.
+ /// Use or to get the horizontal position.
+ ///
+ [JsonIgnore]
public float X
{
set => OriginalXBindable.Value = value;
@@ -34,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float XOffset
{
+ get => XOffsetBindable.Value;
set => XOffsetBindable.Value = value;
}
@@ -44,7 +51,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original value specified in the beatmap, not affected by the beatmap processing.
/// Use for a gameplay.
///
- public float OriginalX => OriginalXBindable.Value;
+ public float OriginalX
+ {
+ get => OriginalXBindable.Value;
+ set => OriginalXBindable.Value = value;
+ }
///
/// The effective horizontal position of the hit object between 0 and .
@@ -53,9 +64,9 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original value plus the offset applied by the beatmap processing.
/// Use if a value not affected by the offset is desired.
///
- public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value;
+ public float EffectiveX => OriginalX + XOffset;
- public double TimePreempt = 1000;
+ public double TimePreempt { get; set; } = 1000;
public readonly Bindable IndexInBeatmapBindable = new Bindable();
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 35fd58826e..3088d024d1 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -25,7 +26,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
+ [JsonIgnore]
public double Velocity { get; private set; }
+
+ [JsonIgnore]
public double TickDistance { get; private set; }
///
@@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public float EndX => OriginalX + this.CurvePositionAt(1).X;
+ [JsonIgnore]
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;
diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
index 0cd3af01df..aa7cabf38b 100644
--- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
@@ -33,6 +34,7 @@ namespace osu.Game.Rulesets.Catch.Objects
///
/// The target fruit if we are to initiate a hyperdash.
///
+ [JsonIgnore]
public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs
new file mode 100644
index 0000000000..ea8d742b1a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinConfiguration.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Catch.Skinning
+{
+ public enum CatchSkinConfiguration
+ {
+ ///
+ /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
+ ///
+ FlipCatcherPlate
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
index b23011f1a3..5e744ec001 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs
@@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
///
private bool providesComboCounter => this.HasFont(LegacyFont.Combo);
- public CatchLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public CatchLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
}
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (targetComponent.Target)
{
case SkinnableTarget.MainHUDComponents:
- var components = Source.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
+ var components = base.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
if (providesComboCounter && components != null)
{
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null;
case CatchSkinComponents.Catcher:
- var version = Source.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
+ var version = GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
if (version < 2.3m)
{
@@ -83,13 +83,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
- return new LegacyCatchComboCounter(Source);
+ return new LegacyCatchComboCounter(Skin);
return null;
}
}
- return Source.GetDrawableComponent(component);
+ return base.GetDrawableComponent(component);
}
public override IBindable GetConfig(TLookup lookup)
@@ -97,15 +97,28 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (lookup)
{
case CatchSkinColour colour:
- var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour));
+ var result = (Bindable)base.GetConfig(new SkinCustomColourLookup(colour));
if (result == null)
return null;
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable)result;
+
+ case CatchSkinConfiguration config:
+ switch (config)
+ {
+ case CatchSkinConfiguration.FlipCatcherPlate:
+ // Don't flip catcher plate contents if the catcher is provided by this legacy skin.
+ if (GetDrawableComponent(new CatchSkinComponent(CatchSkinComponents.Catcher)) != null)
+ return (IBindable)new Bindable();
+
+ break;
+ }
+
+ break;
}
- return Source.GetConfig(lookup);
+ return base.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 644facdabc..05cd29dff5 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -27,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.UI
///
public const float CENTER_X = WIDTH / 2;
+ [Cached]
+ private readonly DroppedObjectContainer droppedObjectContainer;
+
internal readonly CatcherArea CatcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
@@ -35,12 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty)
{
- var droppedObjectContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- };
-
- CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
+ CatcherArea = new CatcherArea(difficulty)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
@@ -48,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new[]
{
- droppedObjectContainer,
+ droppedObjectContainer = new DroppedObjectContainer(),
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(),
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 1f01dbabb5..57523d3505 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -56,11 +56,6 @@ namespace osu.Game.Rulesets.Catch.UI
///
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
- ///
- /// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
- ///
- public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5;
-
///
/// The amount by which caught fruit should be scaled down to fit on the plate.
///
@@ -79,12 +74,13 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// Contains objects dropped from the plate.
///
- private readonly Container droppedObjectTarget;
+ [Resolved]
+ private DroppedObjectContainer droppedObjectTarget { get; set; }
public CatcherAnimationState CurrentState
{
- get => body.AnimationState.Value;
- private set => body.AnimationState.Value = value;
+ get => Body.AnimationState.Value;
+ private set => Body.AnimationState.Value = value;
}
///
@@ -107,18 +103,22 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
- public Direction VisualDirection
- {
- get => Scale.X > 0 ? Direction.Right : Direction.Left;
- set => Scale = new Vector2((value == Direction.Right ? 1 : -1) * Math.Abs(Scale.X), Scale.Y);
- }
+ ///
+ /// The currently facing direction.
+ ///
+ public Direction VisualDirection { get; set; } = Direction.Right;
+
+ ///
+ /// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
+ ///
+ private bool flipCatcherPlate;
///
/// Width of the area that can be used to attempt catches during gameplay.
///
private readonly float catchWidth;
- private readonly SkinnableCatcher body;
+ internal readonly SkinnableCatcher Body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
@@ -134,10 +134,9 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool caughtBananaPool;
private readonly DrawablePool caughtDropletPool;
- public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null)
+ public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
{
this.trailsTarget = trailsTarget;
- this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre;
@@ -157,8 +156,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
+ // offset fruit vertically to better place "above" the plate.
+ Y = -5
},
- body = new SkinnableCatcher(),
+ Body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer
{
Anchor = Anchor.TopCentre,
@@ -347,6 +348,8 @@ namespace osu.Game.Rulesets.Catch.UI
trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
+ flipCatcherPlate = skin.GetConfig(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
+
runHyperDashStateTransition(HyperDashing);
}
@@ -354,6 +357,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
+ var scaleFromDirection = new Vector2((int)VisualDirection, 1);
+ Body.Scale = scaleFromDirection;
+ caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
+
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
(hyperDashDirection < 0 && hyperDashTargetPosition > X))
@@ -388,9 +395,6 @@ namespace osu.Game.Rulesets.Catch.UI
float adjustedRadius = displayRadius * lenience_adjust;
float checkDistance = MathF.Pow(adjustedRadius, 2);
- // offset fruit vertically to better place "above" the plate.
- position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET;
-
while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
{
position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
@@ -465,7 +469,7 @@ namespace osu.Game.Rulesets.Catch.UI
break;
case DroppedObjectAnimation.Explode:
- var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * Scale.X;
+ float originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * caughtObjectContainer.Scale.X;
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750);
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index cdb15c2b4c..fea314df8d 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
private int currentDirection;
- public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null)
+ public CatcherArea(BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[]
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.UI
Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X
},
- MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X },
+ MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
};
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
index 80522ab36b..c961d98dc5 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrail.cs
@@ -37,6 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected override void FreeAfterUse()
{
ClearTransforms();
+ Alpha = 1;
base.FreeAfterUse();
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
index 7e4a5b6a86..b59fabcb70 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI
CatcherTrail sprite = trailPool.Get();
sprite.AnimationState = catcher.CurrentState;
- sprite.Scale = catcher.Scale;
+ sprite.Scale = catcher.Scale * catcher.Body.Scale;
sprite.Position = catcher.Position;
target.Add(sprite);
diff --git a/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
new file mode 100644
index 0000000000..b44b0caae4
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public class DroppedObjectContainer : Container
+ {
+ public DroppedObjectContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index b2a0912d19..6df555617b 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index fbb9b3c466..fe736766d9 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap);
public override IEnumerable ConvertFromLegacyMods(LegacyMods mods)
{
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index 1c89d9cd00..f89750a96e 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Configuration;
@@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
private class TimeSlider : OsuSliderBar
{
- public override string TooltipText => Current.Value.ToString("N0") + "ms";
+ public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index 962a13ebea..814a737034 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -50,29 +50,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ HitResult.Miss, "mania-hit0" }
};
- private Lazy isLegacySkin;
+ private readonly Lazy isLegacySkin;
///
/// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned.
///
- private Lazy hasKeyTexture;
+ private readonly Lazy hasKeyTexture;
- public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
- : base(source)
+ public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin)
{
this.beatmap = (ManiaBeatmap)beatmap;
- Source.SourceChanged += sourceChanged;
- sourceChanged();
- }
-
- private void sourceChanged()
- {
- isLegacySkin = new Lazy(() => FindProvider(s => s.GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null) != null);
- hasKeyTexture = new Lazy(() => FindProvider(s => s.GetAnimation(
- s.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
- ?? "mania-key1", true, true) != null) != null);
+ isLegacySkin = new Lazy(() => GetConfig(LegacySkinConfiguration.LegacySetting.Version) != null);
+ hasKeyTexture = new Lazy(() =>
+ {
+ var keyImage = this.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
+ return this.GetAnimation(keyImage, true, true) != null;
+ });
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@@ -125,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
break;
}
- return Source.GetDrawableComponent(component);
+ return base.GetDrawableComponent(component);
}
private Drawable getResult(HitResult result)
@@ -146,15 +142,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
return new SampleVirtual();
- return Source.GetSample(sampleInfo);
+ return base.GetSample(sampleInfo);
}
public override IBindable GetConfig(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
- return Source.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
+ return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
- return Source.GetConfig(lookup);
+ return base.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index 46274e779b..211b0e8145 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -112,7 +113,9 @@ namespace osu.Game.Rulesets.Osu.Tests
public IBindable GetConfig(TLookup lookup) => null;
- public ISkin FindProvider(Func lookupFunction) => null;
+ public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null;
+
+ public IEnumerable AllSources => new[] { this };
public event Action SourceChanged
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 78bb88322a..2326a0c391 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
case OsuSkinConfiguration osuLookup:
if (osuLookup == OsuSkinConfiguration.CursorCentre)
- return SkinUtils.As(new BindableBool(false));
+ return SkinUtils.As(new BindableBool());
break;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index fd523fffcb..662cbaee68 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -164,9 +165,11 @@ namespace osu.Game.Rulesets.Osu.Tests
public ISample GetSample(ISampleInfo sampleInfo) => null;
- public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default;
public IBindable GetConfig(TLookup lookup) => null;
- public ISkin FindProvider(Func lookupFunction) => null;
+
+ public ISkin FindProvider(Func lookupFunction) => lookupFunction(this) ? this : null;
+
+ public IEnumerable AllSources => new[] { this };
public event Action SourceChanged;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
index e111bb1054..3252e6d912 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs
@@ -37,11 +37,13 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly BindableBool snakingIn = new BindableBool();
private readonly BindableBool snakingOut = new BindableBool();
+ private IBeatmap beatmap;
+
private const double duration_of_span = 3605;
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
- => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
+ => new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
@@ -51,8 +53,16 @@ namespace osu.Game.Rulesets.Osu.Tests
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
+ private Slider slider;
private DrawableSlider drawableSlider;
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ slider = null;
+ drawableSlider = null;
+ });
+
[SetUpSteps]
public override void SetUpSteps()
{
@@ -67,21 +77,19 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
- double startTime = hitObjects[sliderIndex].StartTime;
- addSeekStep(startTime);
- retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
+ retrieveSlider(sliderIndex);
setSnaking(true);
- ensureSnakingIn(startTime + fade_in_modifier);
+ addEnsureSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i < sliderIndex; i++)
{
// non-final repeats should not snake out
- ensureNoSnakingOut(startTime, i);
+ addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
// final repeat should snake out
- ensureSnakingOut(startTime, sliderIndex);
+ addEnsureSnakingOutSteps(() => slider.StartTime, sliderIndex);
}
[TestCase(0)]
@@ -93,17 +101,15 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
- double startTime = hitObjects[sliderIndex].StartTime;
- addSeekStep(startTime);
- retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
+ retrieveSlider(sliderIndex);
setSnaking(false);
- ensureNoSnakingIn(startTime + fade_in_modifier);
+ addEnsureNoSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i <= sliderIndex; i++)
{
// no snaking out ever, including final repeat
- ensureNoSnakingOut(startTime, i);
+ addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
}
@@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// repeat might have a chance to update its position depending on where in the frame its hit,
// so some leniency is allowed here instead of checking strict equality
- checkPositionChange(16600, sliderRepeat, positionAlmostSame);
+ addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
}
[Test]
@@ -126,38 +132,41 @@ namespace osu.Game.Rulesets.Osu.Tests
setSnaking(true);
base.SetUpSteps();
- checkPositionChange(16600, sliderRepeat, positionDecreased);
+ addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
}
- private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () =>
- (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
-
- private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
- private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
-
- private void ensureSnakingOut(double startTime, int repeatIndex)
+ private void retrieveSlider(int index)
{
- var repeatTime = timeAtRepeat(startTime, repeatIndex);
+ AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
+ addSeekStep(() => slider);
+ AddUntilStep("retrieve drawable slider", () =>
+ (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
+ }
+ private void addEnsureSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
+ private void addEnsureNoSnakingInSteps(Func startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionRemainsSame);
+
+ private void addEnsureSnakingOutSteps(Func startTime, int repeatIndex)
+ {
if (repeatIndex % 2 == 0)
- checkPositionChange(repeatTime, sliderStart, positionIncreased);
+ addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderStart, positionIncreased);
else
- checkPositionChange(repeatTime, sliderEnd, positionDecreased);
+ addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderEnd, positionDecreased);
}
- private void ensureNoSnakingOut(double startTime, int repeatIndex) =>
- checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
+ private void addEnsureNoSnakingOutStep(Func startTime, int repeatIndex)
+ => addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
- private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex;
- private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)sliderStart : sliderEnd;
+ private Func timeAtRepeat(Func startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
+ private Func positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func)getSliderStart : getSliderEnd;
- private List sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
- private Vector2 sliderStart() => sliderCurve.First();
- private Vector2 sliderEnd() => sliderCurve.Last();
+ private List getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
+ private Vector2 getSliderStart() => getSliderCurve().First();
+ private Vector2 getSliderEnd() => getSliderCurve().Last();
- private Vector2 sliderRepeat()
+ private Vector2 getSliderRepeat()
{
- var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]);
+ var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == beatmap.HitObjects[1]);
var repeat = drawable.ChildrenOfType>().First().Children.First();
return repeat.Position;
}
@@ -167,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y;
private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1);
- private void checkPositionChange(double startTime, Func positionToCheck, Func positionAssertion)
+ private void addCheckPositionChangeSteps(Func startTime, Func positionToCheck, Func positionAssertion)
{
Vector2 previousPosition = Vector2.Zero;
@@ -176,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(startTime);
AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke());
- addSeekStep(startTime + 100);
+ addSeekStep(() => startTime() + 100);
AddAssert($"{positionDescription} {assertionDescription}", () =>
{
var currentPosition = positionToCheck.Invoke();
@@ -193,19 +202,21 @@ namespace osu.Game.Rulesets.Osu.Tests
});
}
- private void addSeekStep(double time)
+ private void addSeekStep(Func slider)
{
- AddStep($"seek to {time}", () => MusicController.SeekTo(time));
-
- AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime));
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
- protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
+ private void addSeekStep(Func time)
{
- HitObjects = hitObjects
- };
+ AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
+ AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
+ }
- private readonly List hitObjects = new List
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };
+
+ private static List createHitObjects() => new List
{
new Slider
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 8ff21057b5..9da583a073 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time)
{
- AddStep($"seek to {time}", () => MusicController.SeekTo(time));
+ AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 1efd19f49d..68be34d153 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index 48e4db11ca..5b476526c9 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
@@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
- public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
+ public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs
new file mode 100644
index 0000000000..4a3b187e83
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/IHidesApproachCircles.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ ///
+ /// Marker interface for any mod which completely hides the approach circles.
+ /// Used for incompatibility with .
+ ///
+ ///
+ /// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour.
+ ///
+ public interface IHidesApproachCircles
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
deleted file mode 100644
index 60a5825241..0000000000
--- a/osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-namespace osu.Game.Rulesets.Osu.Mods
-{
- ///
- /// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
- ///
- public interface IMutateApproachCircles
- {
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs b/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs
new file mode 100644
index 0000000000..1458abfe05
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/IRequiresApproachCircles.cs
@@ -0,0 +1,16 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ ///
+ /// Marker interface for any mod which requires the approach circles to be visible.
+ /// Used for incompatibility with .
+ ///
+ ///
+ /// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour.
+ ///
+ public interface IRequiresApproachCircles
+ {
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
index 526e29ad53..d832411104 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
@@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
+ public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IRequiresApproachCircles
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index ebf6f9dda7..636cd63c69 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -158,17 +158,17 @@ namespace osu.Game.Rulesets.Osu.Mods
var firstObj = beatmap.HitObjects[0];
var startDelay = firstObj.StartTime - firstObj.TimePreempt;
- using (BeginAbsoluteSequence(startDelay + break_close_late, true))
+ using (BeginAbsoluteSequence(startDelay + break_close_late))
leaveBreak();
foreach (var breakInfo in beatmap.Breaks)
{
if (breakInfo.HasEffect)
{
- using (BeginAbsoluteSequence(breakInfo.StartTime - break_open_early, true))
+ using (BeginAbsoluteSequence(breakInfo.StartTime - break_open_early))
{
enterBreak();
- using (BeginDelayedSequence(breakInfo.Duration + break_open_early + break_close_late, true))
+ using (BeginDelayedSequence(breakInfo.Duration + break_open_early + break_close_late))
leaveBreak();
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 16b38cd0b1..9c7784a00a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -15,12 +15,12 @@ using osu.Game.Rulesets.Osu.Skinning;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModHidden : ModHidden, IMutateApproachCircles
+ public class OsuModHidden : ModHidden, IHidesApproachCircles
{
public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06;
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index 6dfabed0df..778447e444 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
///
/// Adjusts the size of hit objects during their fade in animation.
///
- public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
+ public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IHidesApproachCircles
{
public override ModType Type => ModType.Fun;
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 97e3d82664..d1212096bf 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Osu.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
@@ -23,15 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Description => "It never gets boring!";
- // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
- // The closer the hit objects draw to the border, the sharper the turn
- private const float playfield_edge_ratio = 0.375f;
-
- private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
- private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
-
- private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
-
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random rng;
@@ -113,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Mods
distanceToPrev * (float)Math.Sin(current.AngleRad)
);
- posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev);
+ posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev);
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
@@ -185,73 +177,6 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
- ///
- /// Determines the position of the current hit object relative to the previous one.
- ///
- /// The position of the current hit object relative to the previous one
- private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev)
- {
- var relativeRotationDistance = 0f;
-
- if (prevPosChanged.X < playfield_middle.X)
- {
- relativeRotationDistance = Math.Max(
- (border_distance_x - prevPosChanged.X) / border_distance_x,
- relativeRotationDistance
- );
- }
- else
- {
- relativeRotationDistance = Math.Max(
- (prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
- relativeRotationDistance
- );
- }
-
- if (prevPosChanged.Y < playfield_middle.Y)
- {
- relativeRotationDistance = Math.Max(
- (border_distance_y - prevPosChanged.Y) / border_distance_y,
- relativeRotationDistance
- );
- }
- else
- {
- relativeRotationDistance = Math.Max(
- (prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
- relativeRotationDistance
- );
- }
-
- return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2);
- }
-
- ///
- /// Rotates vector "initial" towards vector "destinantion"
- ///
- /// Vector to rotate to "destination"
- /// Vector "initial" should be rotated to
- /// The angle the vector should be rotated relative to the difference between the angles of the the two vectors.
- /// Resulting vector
- private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance)
- {
- var initialAngleRad = Math.Atan2(initial.Y, initial.X);
- var destAngleRad = Math.Atan2(destination.Y, destination.X);
-
- var diff = destAngleRad - initialAngleRad;
-
- while (diff < -Math.PI) diff += 2 * Math.PI;
-
- while (diff > Math.PI) diff -= 2 * Math.PI;
-
- var finalAngleRad = initialAngleRad + relativeDistance * diff;
-
- return new Vector2(
- initial.Length * (float)Math.Cos(finalAngleRad),
- initial.Length * (float)Math.Sin(finalAngleRad)
- );
- }
-
private class RandomObjectInfo
{
public float AngleRad { get; set; }
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
index d3ca2973f0..95e7d13ee7 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
@@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
+ public class OsuModSpinIn : ModWithVisibilityAdjustment, IHidesApproachCircles
{
public override string Name => "Spin In";
public override string Acronym => "SI";
@@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Circles spin in. No approach circles.";
public override double ScoreMultiplier => 1;
- // todo: this mod should be able to be compatible with hidden with a bit of further implementation.
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ // todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
+ // further implementation will be required for supporting that.
+ public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) };
private const int rotate_offset = 360;
private const float rotate_starting_width = 2;
@@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableHitCircle circle:
- using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
+ using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
{
circle.ApproachCircle.Hide();
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index 84263221a7..07ce009cf8 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
+ public class OsuModTraceable : ModWithVisibilityAdjustment, IRequiresApproachCircles
{
public override string Name => "Traceable";
public override string Acronym => "TC";
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null)
{
var h = hitObject.HitObject;
- using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
+ using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
(hitCircle ?? hitObject).Hide();
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index b5905d7015..8122ab563e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;
double moveDuration = hitObject.TimePreempt + 1;
- using (drawable.BeginAbsoluteSequence(appearTime, true))
+ using (drawable.BeginAbsoluteSequence(appearTime))
{
drawable
.MoveToOffset(appearOffset)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index a01cec4bb3..ff6ba6e121 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
for (int i = 0; i < amountWiggles; i++)
{
- using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration, true))
+ using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration))
wiggle();
}
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Mods
for (int i = 0; i < amountWiggles; i++)
{
- using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration, true))
+ using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration))
wiggle();
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 1b9bcd19fd..5f37b0d040 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new OsuLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new OsuLegacySkinTransformer(skin);
public int LegacyID => 0;
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index 7b0cf651c8..b88bf9108b 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -233,35 +233,43 @@ namespace osu.Game.Rulesets.Osu.Replays
// Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
+ bool hasWaited = false;
if (waitTime > lastFrame.Time)
{
lastFrame = new OsuReplayFrame(waitTime, lastFrame.Position) { Actions = lastFrame.Actions };
+ hasWaited = true;
AddFrameToReplay(lastFrame);
}
- Vector2 lastPosition = lastFrame.Position;
-
double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
+ OsuReplayFrame lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
- // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
- if (timeDifference > 0 && // Sanity checks
- ((lastPosition - targetPos).Length > h.Radius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough
- timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
+ if (timeDifference > 0)
{
- // Perform eased movement
+ // If the last frame is a key-up frame and there has been no wait period, adjust the last frame's position such that it begins eased movement instantaneously.
+ if (lastLastFrame != null && lastFrame is OsuKeyUpReplayFrame && !hasWaited)
+ {
+ // [lastLastFrame] ... [lastFrame] ... [current frame]
+ // We want to find the cursor position at lastFrame, so interpolate between lastLastFrame and the new target position.
+ lastFrame.Position = Interpolation.ValueAt(lastFrame.Time, lastFrame.Position, targetPos, lastLastFrame.Time, h.StartTime, easing);
+ }
+
+ Vector2 lastPosition = lastFrame.Position;
+
+ // Perform the rest of the eased movement until the target position is reached.
for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
}
+ }
- buttonIndex = 0;
- }
- else
- {
+ // Start alternating once the time separation is too small (faster than ~225BPM).
+ if (timeDifference > 0 && timeDifference < 266)
buttonIndex++;
- }
+ else
+ buttonIndex = 0;
}
///
@@ -284,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Replays
// TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime.
double hEndTime = h.GetEndTime() + KEY_UP_DELAY;
int endDelay = h is Spinner ? 1 : 0;
- var endFrame = new OsuReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
+ var endFrame = new OsuKeyUpReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
// Decrement because we want the previous frame, not the next one
int index = FindInsertionIndex(startFrame) - 1;
@@ -381,5 +389,13 @@ namespace osu.Game.Rulesets.Osu.Replays
}
#endregion
+
+ private class OsuKeyUpReplayFrame : OsuReplayFrame
+ {
+ public OsuKeyUpReplayFrame(double time, Vector2 position)
+ : base(time, position)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
index 542f3eff0d..4ea0831627 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs
@@ -130,18 +130,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Spinner spinner = drawableSpinner.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
this.ScaleTo(initial_scale);
this.RotateTo(0);
- using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
- using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
+ using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
{
switch (state)
{
@@ -157,17 +157,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
centre.ScaleTo(0);
mainContainer.ScaleTo(0);
- using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
- using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
@@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
updateComplete(state == ArmedState.Hit, 0);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
index 8feeca56e8..8943a91076 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs
@@ -86,6 +86,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
{
// For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either.
+ // ReSharper disable once RedundantArgumentDefaultValue - removing the "redundant" default value triggers BaseMethodCallWithDefaultParameter
base.ApplyTransformsAt(time, false);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index ae8d6a61f8..1e170036e4 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -100,17 +100,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case DrawableSpinner d:
Spinner spinner = d.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
fixedMiddle.FadeColour(Color4.White);
- using (BeginDelayedSequence(spinner.TimePreempt, true))
+ using (BeginDelayedSequence(spinner.TimePreempt))
fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index cbe721d21d..e3e8f3ce88 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -89,10 +89,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Spinner spinner = d.HitObject;
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 317649785e..93aba608e6 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
///
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
- /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
+ /// This offset is negated to bring all constants into window-space.
/// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
///
protected const float SPINNER_TOP_OFFSET = 45f - 16f;
@@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
double startTime = Math.Min(Time.Current, DrawableSpinner.HitStateUpdateTime - 400);
- using (BeginAbsoluteSequence(startTime, true))
+ using (BeginAbsoluteSequence(startTime))
{
clear.FadeInFromZero(400, Easing.Out);
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
const double fade_out_duration = 50;
- using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration, true))
+ using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration))
clear.FadeOut(fade_out_duration);
}
else
@@ -182,14 +182,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
- using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
+ using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength))
spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:
if (state == ArmedState.Hit)
{
- using (BeginAbsoluteSequence(d.HitStateUpdateTime, true))
+ using (BeginAbsoluteSequence(d.HitStateUpdateTime))
spin.FadeOut(300);
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 3267b48ebf..41b0a88f11 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class OsuLegacySkinTransformer : LegacySkinTransformer
{
- private Lazy hasHitCircle;
+ private readonly Lazy hasHitCircle;
///
/// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
@@ -20,16 +20,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
///
public const float LEGACY_CIRCLE_RADIUS = 64 - 5;
- public OsuLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public OsuLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
- Source.SourceChanged += sourceChanged;
- sourceChanged();
- }
-
- private void sourceChanged()
- {
- hasHitCircle = new Lazy(() => FindProvider(s => s.GetTexture("hitcircle") != null) != null);
+ hasHitCircle = new Lazy(() => GetTexture("hitcircle") != null);
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@@ -49,16 +43,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return followCircle;
case OsuSkinComponents.SliderBall:
- // specular and nd layers must come from the same source as the ball texure.
- var ballProvider = Source.FindProvider(s => s.GetTexture("sliderb") != null || s.GetTexture("sliderb0") != null);
-
- var sliderBallContent = ballProvider.GetAnimation("sliderb", true, true, animationSeparator: "");
+ var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
// todo: slider ball has a custom frame delay based on velocity
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
if (sliderBallContent != null)
- return new LegacySliderBall(sliderBallContent, ballProvider);
+ return new LegacySliderBall(sliderBallContent, this);
return null;
@@ -87,18 +78,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
case OsuSkinComponents.Cursor:
- var cursorProvider = Source.FindProvider(s => s.GetTexture("cursor") != null);
-
- if (cursorProvider != null)
- return new LegacyCursor(cursorProvider);
+ if (GetTexture("cursor") != null)
+ return new LegacyCursor(this);
return null;
case OsuSkinComponents.CursorTrail:
- var trailProvider = Source.FindProvider(s => s.GetTexture("cursortrail") != null);
-
- if (trailProvider != null)
- return new LegacyCursorTrail(trailProvider);
+ if (GetTexture("cursortrail") != null)
+ return new LegacyCursorTrail(this);
return null;
@@ -113,9 +100,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
};
case OsuSkinComponents.SpinnerBody:
- bool hasBackground = Source.GetTexture("spinner-background") != null;
+ bool hasBackground = GetTexture("spinner-background") != null;
- if (Source.GetTexture("spinner-top") != null && !hasBackground)
+ if (GetTexture("spinner-top") != null && !hasBackground)
return new LegacyNewStyleSpinner();
else if (hasBackground)
return new LegacyOldStyleSpinner();
@@ -124,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
}
- return Source.GetDrawableComponent(component);
+ return base.GetDrawableComponent(component);
}
public override IBindable GetConfig(TLookup lookup)
@@ -132,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (lookup)
{
case OsuSkinColour colour:
- return Source.GetConfig(new SkinCustomColourLookup(colour));
+ return base.GetConfig(new SkinCustomColourLookup(colour));
case OsuSkinConfiguration osuLookup:
switch (osuLookup)
@@ -146,14 +133,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
// See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
// HitCircleOverlayAboveNumer (with typo) should still be supported for now.
- return Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
- Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
+ return base.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
+ base.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
}
break;
}
- return Source.GetConfig(lookup);
+ return base.GetConfig(lookup);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
new file mode 100644
index 0000000000..06b964a647
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.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 osu.Game.Rulesets.Osu.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Utils
+{
+ public static class OsuHitObjectGenerationUtils
+ {
+ // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
+ // The closer the hit objects draw to the border, the sharper the turn
+ private const float playfield_edge_ratio = 0.375f;
+
+ private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
+ private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
+
+ private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
+
+ ///
+ /// Rotate a hit object away from the playfield edge, while keeping a constant distance
+ /// from the previous object.
+ ///
+ ///
+ /// The extent of rotation depends on the position of the hit object. Hit objects
+ /// closer to the playfield edge will be rotated to a larger extent.
+ ///
+ /// Position of the previous hit object.
+ /// Position of the hit object to be rotated, relative to the previous hit object.
+ ///
+ /// The extent of rotation.
+ /// 0 means the hit object is never rotated.
+ /// 1 means the hit object will be fully rotated towards playfield center when it is originally at playfield edge.
+ ///
+ /// The new position of the hit object, relative to the previous one.
+ public static Vector2 RotateAwayFromEdge(Vector2 prevObjectPos, Vector2 posRelativeToPrev, float rotationRatio = 0.5f)
+ {
+ var relativeRotationDistance = 0f;
+
+ if (prevObjectPos.X < playfield_middle.X)
+ {
+ relativeRotationDistance = Math.Max(
+ (border_distance_x - prevObjectPos.X) / border_distance_x,
+ relativeRotationDistance
+ );
+ }
+ else
+ {
+ relativeRotationDistance = Math.Max(
+ (prevObjectPos.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
+ relativeRotationDistance
+ );
+ }
+
+ if (prevObjectPos.Y < playfield_middle.Y)
+ {
+ relativeRotationDistance = Math.Max(
+ (border_distance_y - prevObjectPos.Y) / border_distance_y,
+ relativeRotationDistance
+ );
+ }
+ else
+ {
+ relativeRotationDistance = Math.Max(
+ (prevObjectPos.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
+ relativeRotationDistance
+ );
+ }
+
+ return RotateVectorTowardsVector(
+ posRelativeToPrev,
+ playfield_middle - prevObjectPos,
+ Math.Min(1, relativeRotationDistance * rotationRatio)
+ );
+ }
+
+ ///
+ /// Rotates vector "initial" towards vector "destination".
+ ///
+ /// The vector to be rotated.
+ /// The vector that "initial" should be rotated towards.
+ /// How much "initial" should be rotated. 0 means no rotation. 1 means "initial" is fully rotated to equal "destination".
+ /// The rotated vector.
+ public static Vector2 RotateVectorTowardsVector(Vector2 initial, Vector2 destination, float rotationRatio)
+ {
+ var initialAngleRad = MathF.Atan2(initial.Y, initial.X);
+ var destAngleRad = MathF.Atan2(destination.Y, destination.X);
+
+ var diff = destAngleRad - initialAngleRad;
+
+ while (diff < -MathF.PI) diff += 2 * MathF.PI;
+
+ while (diff > MathF.PI) diff -= 2 * MathF.PI;
+
+ var finalAngleRad = initialAngleRad + rotationRatio * diff;
+
+ return new Vector2(
+ initial.Length * MathF.Cos(finalAngleRad),
+ initial.Length * MathF.Sin(finalAngleRad)
+ );
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 8fb167ba10..532fdc5cb0 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index 60f9521996..888f47d341 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.UpdateStartTimeStateTransforms();
- using (BeginDelayedSequence(-ring_appear_offset, true))
+ using (BeginDelayedSequence(-ring_appear_offset))
targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint);
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index 7ce0f6b93b..a3ecbbc436 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Audio;
using osu.Game.Rulesets.Scoring;
@@ -15,18 +14,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class TaikoLegacySkinTransformer : LegacySkinTransformer
{
- private Lazy hasExplosion;
+ private readonly Lazy hasExplosion;
- public TaikoLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public TaikoLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
- Source.SourceChanged += sourceChanged;
- sourceChanged();
- }
-
- private void sourceChanged()
- {
- hasExplosion = new Lazy(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
+ hasExplosion = new Lazy(() => GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@@ -56,7 +49,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.CentreHit:
case TaikoSkinComponents.RimHit:
-
if (GetTexture("taikohitcircle") != null)
return new LegacyHit(taikoComponent.Component);
@@ -91,7 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return null;
case TaikoSkinComponents.TaikoExplosionMiss:
-
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
if (missSprite != null)
return new LegacyHitExplosion(missSprite);
@@ -100,7 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.TaikoExplosionOk:
case TaikoSkinComponents.TaikoExplosionGreat:
-
var hitName = getHitName(taikoComponent.Component);
var hitSprite = this.GetAnimation(hitName, true, false);
@@ -132,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
}
- return Source.GetDrawableComponent(component);
+ return base.GetDrawableComponent(component);
}
private string getHitName(TaikoSkinComponents component)
@@ -155,13 +145,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public override ISample GetSample(ISampleInfo sampleInfo)
{
if (sampleInfo is HitSampleInfo hitSampleInfo)
- return Source.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
+ return base.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
return base.GetSample(sampleInfo);
}
- public override IBindable GetConfig(TLookup lookup) => Source.GetConfig(lookup);
-
private class LegacyTaikoSampleInfo : HitSampleInfo
{
public LegacyTaikoSampleInfo(HitSampleInfo sampleInfo)
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 5854d4770c..ab5fcf6336 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this);
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TaikoLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TaikoLegacySkinTransformer(skin);
public const string SHORT_NAME = "taiko";
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index 0d117f8755..5dc25d6643 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -19,7 +19,9 @@ using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Scoring;
using osu.Game.Tests.Resources;
+using osu.Game.Tests.Scores.IO;
using osu.Game.Users;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
@@ -185,13 +187,62 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
- private string hashFile(string filename)
+ [Test]
+ public async Task TestImportThenImportWithChangedHashedFile()
{
- using (var s = File.OpenRead(filename))
- return s.ComputeMD5Hash();
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
+ {
+ try
+ {
+ var osu = LoadOsuIntoHost(host);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ var imported = await LoadOszIntoOsu(osu);
+
+ await createScoreForBeatmap(osu, imported.Beatmaps.First());
+
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ // arbitrary write to hashed file
+ // this triggers the special BeatmapManager.PreImport deletion/replacement flow.
+ using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
+ await sw.WriteLineAsync("// changed");
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp));
+
+ ensureLoaded(osu);
+
+ // check the newly "imported" beatmap is not the original.
+ Assert.IsTrue(imported.ID != importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
}
[Test]
+ [Ignore("intentionally broken by import optimisations")]
public async Task TestImportThenImportWithChangedFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
@@ -294,6 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[Test]
+ [Ignore("intentionally broken by import optimisations")]
public async Task TestImportCorruptThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
@@ -439,12 +491,11 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
- [TestCase(true)]
- [TestCase(false)]
- public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
+ [Test]
+ public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}"))
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}"))
{
try
{
@@ -452,10 +503,8 @@ namespace osu.Game.Tests.Beatmaps.IO
var imported = await LoadOszIntoOsu(osu);
- if (set)
- imported.OnlineBeatmapSetID = 1234;
- else
- imported.Beatmaps.First().OnlineBeatmapID = 1234;
+ foreach (var b in imported.Beatmaps)
+ b.OnlineBeatmapID = null;
osu.Dependencies.Get().Update(imported);
@@ -895,7 +944,17 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
}
- private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
+ private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap)
+ {
+ return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
+ {
+ OnlineScoreID = 2,
+ Beatmap = beatmap,
+ BeatmapInfoID = beatmap.ID
+ }, new ImportScoreTest.TestArchiveReader());
+ }
+
+ private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
{
var manager = osu.Dependencies.Get();
@@ -904,12 +963,18 @@ namespace osu.Game.Tests.Beatmaps.IO
: manager.GetAllUsableBeatmapSets().Count);
}
- private void checkBeatmapCount(OsuGameBase osu, int expected)
+ private static string hashFile(string filename)
+ {
+ using (var s = File.OpenRead(filename))
+ return s.ComputeMD5Hash();
+ }
+
+ private static void checkBeatmapCount(OsuGameBase osu, int expected)
{
Assert.AreEqual(expected, osu.Dependencies.Get().QueryBeatmaps(_ => true).ToList().Count);
}
- private void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
+ private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
{
Assert.AreEqual(expected, osu.Dependencies.Get().QueryFiles(f => f.ReferenceCount == 1).Count());
}
diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
index 6c8133660f..9fba0f1668 100644
--- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
+++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestSingleSpan()
{
- var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestRepeat()
{
- var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestNonEvenTicks()
{
- var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestLegacyLastTickOffset()
{
- var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray();
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
Assert.That(events[2].Time, Is.EqualTo(900));
@@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
const double velocity = 5;
const double min_distance = velocity * 10;
- var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, default).ToArray();
+ var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray();
Assert.Multiple(() =>
{
diff --git a/osu.Game.Tests/Chat/TestSceneChannelManager.cs b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
new file mode 100644
index 0000000000..0ec21a4c7b
--- /dev/null
+++ b/osu.Game.Tests/Chat/TestSceneChannelManager.cs
@@ -0,0 +1,105 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.Chat;
+using osu.Game.Tests.Visual;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Chat
+{
+ [HeadlessTest]
+ public class TestSceneChannelManager : OsuTestScene
+ {
+ private ChannelManager channelManager;
+ private int currentMessageId;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ var container = new ChannelManagerContainer();
+ Child = container;
+ channelManager = container.ChannelManager;
+ });
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("register request handling", () =>
+ {
+ currentMessageId = 0;
+
+ ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case JoinChannelRequest joinChannel:
+ joinChannel.TriggerSuccess();
+ return true;
+
+ case PostMessageRequest postMessage:
+ postMessage.TriggerSuccess(new Message(++currentMessageId)
+ {
+ IsAction = postMessage.Message.IsAction,
+ ChannelId = postMessage.Message.ChannelId,
+ Content = postMessage.Message.Content,
+ Links = postMessage.Message.Links,
+ Timestamp = postMessage.Message.Timestamp,
+ Sender = postMessage.Message.Sender
+ });
+
+ return true;
+ }
+
+ return false;
+ };
+ });
+ }
+
+ [Test]
+ public void TestCommandsPostedToCorrectChannelWhenNotCurrent()
+ {
+ Channel channel1 = null;
+ Channel channel2 = null;
+
+ AddStep("join 2 rooms", () =>
+ {
+ channelManager.JoinChannel(channel1 = createChannel(1, ChannelType.Public));
+ channelManager.JoinChannel(channel2 = createChannel(2, ChannelType.Public));
+ });
+
+ AddStep("select channel 1", () => channelManager.CurrentChannel.Value = channel1);
+
+ AddStep("post /me command to channel 2", () => channelManager.PostCommand("me dances", channel2));
+ AddAssert("/me command received by channel 2", () => channel2.Messages.Last().Content == "dances");
+
+ AddStep("post /np command to channel 2", () => channelManager.PostCommand("np", channel2));
+ AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
+ }
+
+ private Channel createChannel(int id, ChannelType type) => new Channel(new User())
+ {
+ Id = id,
+ Name = $"Channel {id}",
+ Topic = $"Topic of channel {id} with type {type}",
+ Type = type,
+ };
+
+ private class ChannelManagerContainer : CompositeDrawable
+ {
+ [Cached]
+ public ChannelManager ChannelManager { get; } = new ChannelManager();
+
+ public ChannelManagerContainer()
+ {
+ InternalChild = ChannelManager;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
index cac331451b..642ecf00b8 100644
--- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
+++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
@@ -38,19 +38,28 @@ namespace osu.Game.Tests.Database
[Test]
public void TestDefaultsPopulationAndQuery()
{
- Assert.That(query().Count, Is.EqualTo(0));
+ Assert.That(queryCount(), Is.EqualTo(0));
KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer);
- Assert.That(query().Count, Is.EqualTo(3));
+ Assert.That(queryCount(), Is.EqualTo(3));
- Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1));
- Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2));
+ Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
+ Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2));
}
- private IQueryable query() => realmContextFactory.Context.All();
+ private int queryCount(GlobalAction? match = null)
+ {
+ using (var usage = realmContextFactory.GetForRead())
+ {
+ var results = usage.Realm.All();
+ if (match.HasValue)
+ results = results.Where(k => k.ActionInt == (int)match.Value);
+ return results.Count();
+ }
+ }
[Test]
public void TestUpdateViaQueriedReference()
@@ -59,25 +68,28 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer);
- var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
-
- Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
-
- var tsr = ThreadSafeReference.Create(backBinding);
-
- using (var usage = realmContextFactory.GetForWrite())
+ using (var primaryUsage = realmContextFactory.GetForRead())
{
- var binding = usage.Realm.ResolveReference(tsr);
- binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
+ var backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back);
- usage.Commit();
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
+
+ var tsr = ThreadSafeReference.Create(backBinding);
+
+ using (var usage = realmContextFactory.GetForWrite())
+ {
+ var binding = usage.Realm.ResolveReference(tsr);
+ binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
+
+ usage.Commit();
+ }
+
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
+
+ // check still correct after re-query.
+ backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back);
+ Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
-
- Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
-
- // check still correct after re-query.
- backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
- Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
[TearDown]
diff --git a/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs
new file mode 100644
index 0000000000..cf5b3a42a4
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs
@@ -0,0 +1,241 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckFewHitsoundsTest
+ {
+ private CheckFewHitsounds check;
+
+ private List notHitsounded;
+ private List hitsounded;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckFewHitsounds();
+ notHitsounded = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+ hitsounded = new List
+ {
+ new HitSampleInfo(HitSampleInfo.HIT_NORMAL),
+ new HitSampleInfo(HitSampleInfo.HIT_FINISH)
+ };
+ }
+
+ [Test]
+ public void TestHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 16; ++i)
+ {
+ var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+
+ if ((i + 1) % 2 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
+ if ((i + 1) % 3 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
+ if ((i + 1) % 4 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertOk(hitObjects);
+ }
+
+ [Test]
+ public void TestHitsoundedWithBreak()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 32; ++i)
+ {
+ var samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
+
+ if ((i + 1) % 2 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
+ if ((i + 1) % 3 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
+ if ((i + 1) % 4 == 0)
+ samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
+ // Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue.
+ if (i > 8 && i < 24)
+ continue;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertOk(hitObjects);
+ }
+
+ [Test]
+ public void TestLightlyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 30; ++i)
+ {
+ var samples = i % 8 == 0 ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ assertLongPeriodNegligible(hitObjects, count: 3);
+ }
+
+ [Test]
+ public void TestRarelyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 30; ++i)
+ {
+ var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ // Should prompt one warning between 1st and 16th, and another between 16th and 31st.
+ assertLongPeriodWarning(hitObjects, count: 2);
+ }
+
+ [Test]
+ public void TestExtremelyRarelyHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 80; ++i)
+ {
+ var samples = i == 40 ? hitsounded : notHitsounded;
+
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
+ }
+
+ // Should prompt one problem between 1st and 41st, and another between 41st and 81st.
+ assertLongPeriodProblem(hitObjects, count: 2);
+ }
+
+ [Test]
+ public void TestNotHitsounded()
+ {
+ var hitObjects = new List();
+
+ for (int i = 0; i < 20; ++i)
+ hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded });
+
+ assertNoHitsounds(hitObjects);
+ }
+
+ [Test]
+ public void TestNestedObjectsHitsounded()
+ {
+ var ticks = new List();
+ for (int i = 1; i < 16; ++i)
+ ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
+ {
+ Samples = hitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertOk(new List { nested });
+ }
+
+ [Test]
+ public void TestNestedObjectsRarelyHitsounded()
+ {
+ var ticks = new List();
+ for (int i = 1; i < 16; ++i)
+ ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
+ {
+ Samples = hitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ assertLongPeriodWarning(new List { nested });
+ }
+
+ [Test]
+ public void TestConcurrentObjects()
+ {
+ var hitObjects = new List();
+
+ var ticks = new List();
+ for (int i = 1; i < 10; ++i)
+ ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded });
+
+ var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000)
+ {
+ Samples = notHitsounded
+ };
+ nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+ hitObjects.Add(nested);
+
+ for (int i = 1; i <= 6; ++i)
+ hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded });
+
+ assertOk(hitObjects);
+ }
+
+ private void assertOk(List hitObjects)
+ {
+ Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
+ }
+
+ private void assertLongPeriodProblem(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem));
+ }
+
+ private void assertLongPeriodWarning(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning));
+ }
+
+ private void assertLongPeriodNegligible(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible));
+ }
+
+ private void assertNoHitsounds(List hitObjects)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects)
+ {
+ var beatmap = new Beatmap { HitObjects = hitObjects };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
new file mode 100644
index 0000000000..41a8f72305
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
@@ -0,0 +1,289 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckMutedObjectsTest
+ {
+ private CheckMutedObjects check;
+ private ControlPointInfo cpi;
+
+ private const int volume_regular = 50;
+ private const int volume_low = 15;
+ private const int volume_muted = 5;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckMutedObjects();
+
+ cpi = new ControlPointInfo();
+ cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
+ cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
+ cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
+ }
+
+ [Test]
+ public void TestNormalControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestLowControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 1000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertLowVolume(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestNormalSampleVolume()
+ {
+ // The sample volume should take precedence over the control point volume.
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestLowSampleVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertLowVolume(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestMutedSampleVolume()
+ {
+ var hitcircle = new HitCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
+ };
+ hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { hitcircle });
+ }
+
+ [Test]
+ public void TestNormalSampleVolumeSlider()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine.
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertOk(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedSampleVolumeSliderHead()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedSampleVolumeSliderTail()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail.
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMutedPassive(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolumeSliderHead()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 2000,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 2250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMuted(new List { slider });
+ }
+
+ [Test]
+ public void TestMutedControlPointVolumeSliderTail()
+ {
+ var sliderHead = new SliderHeadCircle
+ {
+ StartTime = 0,
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ var sliderTick = new SliderTick
+ {
+ StartTime = 250,
+ Samples = new List { new HitSampleInfo("slidertick") }
+ };
+ sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ // Ends after the 5% control point.
+ var slider = new MockNestableHitObject(new List { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
+ {
+ Samples = new List { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ };
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty());
+
+ assertMutedPassive(new List { slider });
+ }
+
+ private void assertOk(List hitObjects)
+ {
+ Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
+ }
+
+ private void assertLowVolume(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive));
+ }
+
+ private void assertMuted(List hitObjects, int count = 1)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(count));
+ Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive));
+ }
+
+ private void assertMutedPassive(List hitObjects)
+ {
+ var issues = check.Run(getContext(hitObjects)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive));
+ }
+
+ private BeatmapVerifierContext getContext(List hitObjects)
+ {
+ var beatmap = new Beatmap
+ {
+ ControlPointInfo = cpi,
+ HitObjects = hitObjects
+ };
+
+ return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs
new file mode 100644
index 0000000000..29938839d3
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs
@@ -0,0 +1,36 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Threading;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ public sealed class MockNestableHitObject : HitObject, IHasDuration
+ {
+ private readonly IEnumerable toBeNested;
+
+ public MockNestableHitObject(IEnumerable toBeNested, double startTime, double endTime)
+ {
+ this.toBeNested = toBeNested;
+ StartTime = startTime;
+ EndTime = endTime;
+ }
+
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ foreach (var hitObject in toBeNested)
+ AddNested(hitObject);
+ }
+
+ public double EndTime { get; }
+
+ public double Duration
+ {
+ get => EndTime - StartTime;
+ set => throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs
index ea351e0d45..e888f51e98 100644
--- a/osu.Game.Tests/ImportTest.cs
+++ b/osu.Game.Tests/ImportTest.cs
@@ -17,7 +17,8 @@ namespace osu.Game.Tests
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
- Task.Run(() => host.Run(osu));
+ Task.Run(() => host.Run(osu))
+ .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
diff --git a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs
new file mode 100644
index 0000000000..dab4825919
--- /dev/null
+++ b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.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 NUnit.Framework;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Tests.Localisation
+{
+ [TestFixture]
+ public class BeatmapMetadataRomanisationTest
+ {
+ [Test]
+ public void TestRomanisation()
+ {
+ var metadata = new BeatmapMetadata
+ {
+ Artist = "Romanised Artist",
+ ArtistUnicode = "Unicode Artist",
+ Title = "Romanised title",
+ TitleUnicode = "Unicode Title"
+ };
+ var romanisableString = metadata.ToRomanisableString();
+
+ Assert.AreEqual(metadata.ToString(), romanisableString.Romanised);
+ Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original);
+ }
+
+ [Test]
+ public void TestRomanisationNoUnicode()
+ {
+ var metadata = new BeatmapMetadata
+ {
+ Artist = "Romanised Artist",
+ Title = "Romanised title"
+ };
+ var romanisableString = metadata.ToRomanisableString();
+
+ Assert.AreEqual(romanisableString.Romanised, romanisableString.Original);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs
index 9f27289d7e..4c126f0a3b 100644
--- a/osu.Game.Tests/Mods/ModUtilsTest.cs
+++ b/osu.Game.Tests/Mods/ModUtilsTest.cs
@@ -14,6 +14,14 @@ namespace osu.Game.Tests.Mods
[TestFixture]
public class ModUtilsTest
{
+ [Test]
+ public void TestModIsNotCompatibleWithItself()
+ {
+ var mod = new Mock();
+ Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object, mod.Object }, out var invalid), Is.False);
+ Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object }));
+ }
+
[Test]
public void TestModIsCompatibleByItself()
{
@@ -147,7 +155,7 @@ namespace osu.Game.Tests.Mods
// multi mod.
new object[]
{
- new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() },
+ new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() },
new[] { typeof(MultiMod) }
},
// valid pair.
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
index 9bd262a569..a55bdd2df8 100644
--- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -90,6 +90,20 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
}
+ [Test]
+ public void TestApplyOverallDifficultyQueries()
+ {
+ const string query = "od>4 easy od<8";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.Greater(filterCriteria.OverallDifficulty.Min, 4.0);
+ Assert.Less(filterCriteria.OverallDifficulty.Min, 4.1);
+ Assert.Greater(filterCriteria.OverallDifficulty.Max, 7.9);
+ Assert.Less(filterCriteria.OverallDifficulty.Max, 8.0);
+ }
+
[Test]
public void TestApplyBPMQueries()
{
diff --git a/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
new file mode 100644
index 0000000000..97105b6b6a
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/FirstAvailableHitWindowsTest.cs
@@ -0,0 +1,123 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Scoring;
+
+namespace osu.Game.Tests.NonVisual
+{
+ public class FirstAvailableHitWindowsTest
+ {
+ private TestDrawableRuleset testDrawableRuleset;
+
+ [SetUp]
+ public void Setup()
+ {
+ testDrawableRuleset = new TestDrawableRuleset();
+ }
+
+ [Test]
+ public void TestResultIfOnlyParentHitWindowIsEmpty()
+ {
+ var testObject = new TestHitObject(HitWindows.Empty);
+ HitObject nested = new TestHitObject(new HitWindows());
+ testObject.AddNested(nested);
+ testDrawableRuleset.HitObjects = new List { testObject };
+
+ Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, nested.HitWindows);
+ }
+
+ [Test]
+ public void TestResultIfParentHitWindowsIsNotEmpty()
+ {
+ var testObject = new TestHitObject(new HitWindows());
+ HitObject nested = new TestHitObject(new HitWindows());
+ testObject.AddNested(nested);
+ testDrawableRuleset.HitObjects = new List { testObject };
+
+ Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, testObject.HitWindows);
+ }
+
+ [Test]
+ public void TestResultIfParentAndChildHitWindowsAreEmpty()
+ {
+ var firstObject = new TestHitObject(HitWindows.Empty);
+ HitObject nested = new TestHitObject(HitWindows.Empty);
+ firstObject.AddNested(nested);
+
+ var secondObject = new TestHitObject(new HitWindows());
+ testDrawableRuleset.HitObjects = new List { firstObject, secondObject };
+
+ Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows);
+ }
+
+ [Test]
+ public void TestResultIfAllHitWindowsAreEmpty()
+ {
+ var firstObject = new TestHitObject(HitWindows.Empty);
+ HitObject nested = new TestHitObject(HitWindows.Empty);
+ firstObject.AddNested(nested);
+
+ testDrawableRuleset.HitObjects = new List { firstObject };
+
+ Assert.IsNull(testDrawableRuleset.FirstAvailableHitWindows);
+ }
+
+ [SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")]
+ private class TestDrawableRuleset : DrawableRuleset
+ {
+ public List HitObjects;
+ public override IEnumerable Objects => HitObjects;
+
+ public override event Action NewResult;
+ public override event Action RevertResult;
+
+ public override Playfield Playfield { get; }
+ public override Container Overlays { get; }
+ public override Container FrameStableComponents { get; }
+ public override IFrameStableClock FrameStableClock { get; }
+ internal override bool FrameStablePlayback { get; set; }
+ public override IReadOnlyList Mods { get; }
+
+ public override double GameplayStartTime { get; }
+ public override GameplayCursorContainer Cursor { get; }
+
+ public TestDrawableRuleset()
+ : base(new OsuRuleset())
+ {
+ // won't compile without this.
+ NewResult?.Invoke(null);
+ RevertResult?.Invoke(null);
+ }
+
+ public override void SetReplayScore(Score replayScore) => throw new NotImplementedException();
+
+ public override void SetRecordTarget(Score score) => throw new NotImplementedException();
+
+ public override void RequestResume(Action continueResume) => throw new NotImplementedException();
+
+ public override void CancelResume() => throw new NotImplementedException();
+ }
+
+ public class TestHitObject : HitObject
+ {
+ public TestHitObject(HitWindows hitWindows)
+ {
+ HitWindows = hitWindows;
+ HitWindows.SetDifficulty(0.5f);
+ }
+
+ public new void AddNested(HitObject nested) => base.AddNested(nested);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
index adc1d6aede..0983b806e2 100644
--- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
+++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
+using osu.Game.Online.Rooms;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Users;
@@ -50,7 +51,10 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("create room initially in gameplay", () =>
{
- Room.RoomID.Value = null;
+ var newRoom = new Room();
+ newRoom.CopyFrom(SelectedRoom.Value);
+
+ newRoom.RoomID.Value = null;
Client.RoomSetupAction = room =>
{
room.State = MultiplayerRoomState.Playing;
@@ -61,7 +65,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
});
};
- RoomManager.CreateRoom(Room);
+ RoomManager.CreateRoom(newRoom);
});
AddUntilStep("wait for room join", () => Client.Room != null);
diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
index d4e591cf09..6851df3832 100644
--- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
+++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
@@ -31,32 +31,24 @@ namespace osu.Game.Tests.OnlinePlay
}
[Test]
- public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames()
+ public void TestPlayerClocksStartWhenAllHaveFrames()
{
setWaiting(() => player1, false);
- assertMasterState(false);
assertPlayerClockState(() => player1, false);
assertPlayerClockState(() => player2, false);
setWaiting(() => player2, false);
- assertMasterState(true);
assertPlayerClockState(() => player1, true);
assertPlayerClockState(() => player2, true);
}
[Test]
- public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime()
- {
- AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
- assertMasterState(false);
- }
-
- [Test]
- public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime()
+ public void TestReadyPlayersStartWhenReadyForMaximumDelayTime()
{
setWaiting(() => player1, false);
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
- assertMasterState(true);
+ assertPlayerClockState(() => player1, true);
+ assertPlayerClockState(() => player2, false);
}
[Test]
@@ -153,9 +145,6 @@ namespace osu.Game.Tests.OnlinePlay
private void setPlayerClockTime(Func playerClock, double offsetFromMaster)
=> AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
- private void assertMasterState(bool running)
- => AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running);
-
private void assertCatchingUp(Func playerClock, bool catchingUp) =>
AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
@@ -201,6 +190,11 @@ namespace osu.Game.Tests.OnlinePlay
private class TestManualClock : ManualClock, IAdjustableClock
{
+ public TestManualClock()
+ {
+ IsRunning = true;
+ }
+
public void Start() => IsRunning = true;
public void Stop() => IsRunning = false;
diff --git a/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
new file mode 100644
index 0000000000..28ad7ed6a7
--- /dev/null
+++ b/osu.Game.Tests/Rulesets/TestSceneRulesetSkinProvidingContainer.cs
@@ -0,0 +1,89 @@
+// 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.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
+using osu.Game.Audio;
+using osu.Game.Rulesets;
+using osu.Game.Skinning;
+using osu.Game.Tests.Testing;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Rulesets
+{
+ [HeadlessTest]
+ public class TestSceneRulesetSkinProvidingContainer : OsuTestScene
+ {
+ private SkinRequester requester;
+
+ protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset();
+
+ [Test]
+ public void TestRulesetResources()
+ {
+ setupProviderStep();
+
+ AddAssert("ruleset texture retrieved via skin", () => requester.GetTexture("test-image") != null);
+ AddAssert("ruleset sample retrieved via skin", () => requester.GetSample(new SampleInfo("test-sample")) != null);
+ }
+
+ [Test]
+ public void TestEarlyAddedSkinRequester()
+ {
+ Texture textureOnLoad = null;
+
+ AddStep("setup provider", () =>
+ {
+ var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin);
+
+ rulesetSkinProvider.Add(requester = new SkinRequester());
+
+ requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image");
+
+ Child = rulesetSkinProvider;
+ });
+
+ AddAssert("requester got correct initial texture", () => textureOnLoad != null);
+ }
+
+ private void setupProviderStep()
+ {
+ AddStep("setup provider", () =>
+ {
+ Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin)
+ .WithChild(requester = new SkinRequester());
+ });
+ }
+
+ private class SkinRequester : Drawable, ISkin
+ {
+ private ISkinSource skin;
+
+ public event Action OnLoadAsync;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ this.skin = skin;
+
+ OnLoadAsync?.Invoke();
+ }
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
+
+ public Texture GetTexture(string componentName, WrapMode wrapModeS = default, WrapMode wrapModeT = default) => skin.GetTexture(componentName);
+
+ public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
+
+ public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
index 7522aca5dc..cd7d744f53 100644
--- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
+++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO
OnlineScoreID = 12345,
};
- var imported = await loadScoreIntoOsu(osu, toImport);
+ var imported = await LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
@@ -75,7 +75,7 @@ namespace osu.Game.Tests.Scores.IO
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
};
- var imported = await loadScoreIntoOsu(osu, toImport);
+ var imported = await LoadScoreIntoOsu(osu, toImport);
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
@@ -105,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO
}
};
- var imported = await loadScoreIntoOsu(osu, toImport);
+ var imported = await LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]);
Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]);
@@ -136,7 +136,7 @@ namespace osu.Game.Tests.Scores.IO
}
};
- var imported = await loadScoreIntoOsu(osu, toImport);
+ var imported = await LoadScoreIntoOsu(osu, toImport);
var beatmapManager = osu.Dependencies.Get();
var scoreManager = osu.Dependencies.Get();
@@ -144,7 +144,7 @@ namespace osu.Game.Tests.Scores.IO
beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID)));
Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true));
- var secondImport = await loadScoreIntoOsu(osu, imported);
+ var secondImport = await LoadScoreIntoOsu(osu, imported);
Assert.That(secondImport, Is.Null);
}
finally
@@ -163,7 +163,7 @@ namespace osu.Game.Tests.Scores.IO
{
var osu = LoadOsuIntoHost(host, true);
- await loadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
+ await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
var scoreManager = osu.Dependencies.Get();
@@ -177,7 +177,7 @@ namespace osu.Game.Tests.Scores.IO
}
}
- private async Task loadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
+ public static async Task LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{
var beatmapManager = osu.Dependencies.Get();
@@ -190,7 +190,7 @@ namespace osu.Game.Tests.Scores.IO
return scoreManager.GetAllUsableScores().FirstOrDefault();
}
- private class TestArchiveReader : ArchiveReader
+ internal class TestArchiveReader : ArchiveReader
{
public TestArchiveReader()
: base("test_archive")
diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
new file mode 100644
index 0000000000..cfc4ccd208
--- /dev/null
+++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
@@ -0,0 +1,92 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Tests.Skins
+{
+ public class TestSceneSkinProvidingContainer : OsuTestScene
+ {
+ ///
+ /// Ensures that the first inserted skin after resetting (via source change)
+ /// is always prioritised over others when providing the same resource.
+ ///
+ [Test]
+ public void TestPriorityPreservation()
+ {
+ TestSkinProvidingContainer provider = null;
+ TestSkin mostPrioritisedSource = null;
+
+ AddStep("setup sources", () =>
+ {
+ var sources = new List();
+ for (int i = 0; i < 10; i++)
+ sources.Add(new TestSkin());
+
+ mostPrioritisedSource = sources.First();
+
+ Child = provider = new TestSkinProvidingContainer(sources);
+ });
+
+ AddAssert("texture provided by expected skin", () =>
+ {
+ return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
+ });
+
+ AddStep("trigger source change", () => provider.TriggerSourceChanged());
+
+ AddAssert("texture still provided by expected skin", () =>
+ {
+ return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
+ });
+ }
+
+ private class TestSkinProvidingContainer : SkinProvidingContainer
+ {
+ private readonly IEnumerable sources;
+
+ public TestSkinProvidingContainer(IEnumerable sources)
+ {
+ this.sources = sources;
+ }
+
+ public new void TriggerSourceChanged() => base.TriggerSourceChanged();
+
+ protected override void OnSourceChanged()
+ {
+ ResetSources();
+ sources.ForEach(AddSource);
+ }
+ }
+
+ private class TestSkin : ISkin
+ {
+ public const string TEXTURE_NAME = "virtual-texture";
+
+ public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException();
+
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
+ {
+ if (componentName == TEXTURE_NAME)
+ return Texture.WhitePixel;
+
+ return null;
+ }
+
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new System.NotImplementedException();
+
+ public IBindable GetConfig(TLookup lookup) => throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
index 433022788b..8c6932e792 100644
--- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
+++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Tests.Testing
Dependencies.Get() != null);
}
- private class TestRuleset : Ruleset
+ public class TestRuleset : Ruleset
{
public override string Description => string.Empty;
public override string ShortName => string.Empty;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index 07162c3cd1..b6ae91844a 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -55,7 +55,12 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestExitWithoutSave()
{
- AddStep("exit without save", () => Editor.Exit());
+ AddStep("exit without save", () =>
+ {
+ Editor.Exit();
+ DialogOverlay.CurrentDialog.PerformOkAction();
+ });
+
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
index cc53e50884..13e84e335d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -116,12 +116,12 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestOsuRuleset : OsuRuleset
{
- public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(source);
+ public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TestOsuLegacySkinTransformer(skin);
private class TestOsuLegacySkinTransformer : OsuLegacySkinTransformer
{
- public TestOsuLegacySkinTransformer(ISkinSource source)
- : base(source)
+ public TestOsuLegacySkinTransformer(ISkin skin)
+ : base(skin)
{
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
index 4ee48fd853..11bd701e19 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs
@@ -114,11 +114,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public bool ResultsCreated { get; private set; }
- public FakeRankingPushPlayer()
- : base(true, true)
- {
- }
-
protected override ResultsScreen CreateResults(ScoreInfo score)
{
var results = base.CreateResults(score);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
new file mode 100644
index 0000000000..c3a46ec4ac
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -0,0 +1,194 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Online.Solo;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Ranking;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestScenePlayerScoreSubmission : OsuPlayerTestScene
+ {
+ protected override bool AllowFail => allowFail;
+
+ private bool allowFail;
+
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
+
+ protected override bool HasCustomSteps => true;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = (TestBeatmap)base.CreateBeatmap(ruleset);
+
+ beatmap.HitObjects = beatmap.HitObjects.Take(10).ToList();
+
+ return beatmap;
+ }
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
+
+ [Test]
+ public void TestNoSubmissionOnResultsWithNoToken()
+ {
+ prepareTokenResponse(false);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestSubmissionOnResults()
+ {
+ prepareTokenResponse(true);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnExitWithNoToken()
+ {
+ prepareTokenResponse(false);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnEmptyFail()
+ {
+ prepareTokenResponse(true);
+
+ CreateTest(() => allowFail = true);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddStep("exit", () => Player.Exit());
+
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestSubmissionOnFail()
+ {
+ prepareTokenResponse(true);
+
+ CreateTest(() => allowFail = true);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ addFakeHit();
+
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddStep("exit", () => Player.Exit());
+
+ AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
+ }
+
+ [Test]
+ public void TestNoSubmissionOnEmptyExit()
+ {
+ prepareTokenResponse(true);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure no submission", () => Player.SubmittedScore == null);
+ }
+
+ [Test]
+ public void TestSubmissionOnExit()
+ {
+ prepareTokenResponse(true);
+
+ CreateTest(() => allowFail = false);
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ addFakeHit();
+
+ AddStep("exit", () => Player.Exit());
+ AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
+ }
+
+ private void addFakeHit()
+ {
+ AddUntilStep("wait for first result", () => Player.Results.Count > 0);
+
+ AddStep("force successfuly hit", () =>
+ {
+ Player.ScoreProcessor.RevertResult(Player.Results.First());
+ Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new OsuJudgement())
+ {
+ Type = HitResult.Great,
+ });
+ });
+ }
+
+ private void prepareTokenResponse(bool validToken)
+ {
+ AddStep("Prepare test API", () =>
+ {
+ dummyAPI.HandleRequest = request =>
+ {
+ switch (request)
+ {
+ case CreateSoloScoreRequest tokenRequest:
+ if (validToken)
+ tokenRequest.TriggerSuccess(new APIScoreToken { ID = 1234 });
+ else
+ tokenRequest.TriggerFailure(new APIException("something went wrong!", null));
+ return true;
+ }
+
+ return false;
+ };
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index 96418f6d28..3e8ba69e01 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
@@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Children = new[]
{
new ExposedSkinnableDrawable("default", _ => new DefaultBox()),
- new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.ScaleToFit),
+ new ExposedSkinnableDrawable("available", _ => new DefaultBox()),
new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.NoScaling)
}
},
@@ -330,6 +331,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public ISkin FindProvider(Func lookupFunction) => throw new NotImplementedException();
+ public IEnumerable