diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 33ec3d6602..56b3ebe87b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -88,7 +88,7 @@ jobs:
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test
- run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
+ run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
shell: pwsh
# Attempt to upload results even if test fails.
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 b8604169aa..936808f38b 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
@@ -9,9 +9,9 @@
false
-
+
-
+
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 4117452579..35e7742172 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
@@ -9,9 +9,9 @@
false
-
+
-
+
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 0b119c8680..c1044965b5 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
@@ -9,9 +9,9 @@
false
-
+
-
+
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 4117452579..35e7742172 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
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/osu.Android.props b/osu.Android.props
index dd263d6aaa..3f4c8e2d24 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
-
-
+
+
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index d9ad95f96a..3ee1b3da30 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -137,12 +137,13 @@ namespace osu.Desktop
{
base.SetHost(host);
- var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
-
var desktopWindow = (SDL2DesktopWindow)host.Window;
+ var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
+ if (iconStream != null)
+ desktopWindow.SetIconFromStream(iconStream);
+
desktopWindow.CursorState |= CursorState.Hidden;
- desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
}
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 36ffd3b5b6..d62d422f33 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -7,9 +7,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
index cbf6e8f202..cf6a8169c4 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchTouchInput.cs
@@ -1,10 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
+using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
@@ -12,11 +17,11 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture]
public class TestSceneCatchTouchInput : OsuTestScene
{
- private CatchTouchInputMapper catchTouchInputMapper = null!;
-
- [SetUpSteps]
- public void SetUpSteps()
+ [Test]
+ public void TestBasic()
{
+ CatchTouchInputMapper catchTouchInputMapper = null!;
+
AddStep("create input overlay", () =>
{
Child = new CatchInputManager(new CatchRuleset().RulesetInfo)
@@ -32,12 +37,30 @@ namespace osu.Game.Rulesets.Catch.Tests
}
};
});
+
+ AddStep("show overlay", () => catchTouchInputMapper.Show());
}
[Test]
- public void TestBasic()
+ public void TestWithoutRelax()
{
- AddStep("show overlay", () => catchTouchInputMapper.Show());
+ AddStep("create drawable ruleset without relax mod", () =>
+ {
+ Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List());
+ });
+ AddUntilStep("wait for load", () => Child.IsLoaded);
+ AddAssert("check touch input is shown", () => this.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestWithRelax()
+ {
+ AddStep("create drawable ruleset with relax mod", () =>
+ {
+ Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List { new CatchModRelax() });
+ });
+ AddUntilStep("wait for load", () => Child.IsLoaded);
+ AddAssert("check touch input is not shown", () => !this.ChildrenOfType().Any());
}
}
}
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 d45f8a9692..c9db824615 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
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
index 58f493b4b8..a0a11424d0 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBlueprintContainer.cs
@@ -36,5 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return base.CreateHitObjectBlueprintFor(hitObject);
}
+
+ protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
index 1adc969f8f..a9e9e8fbd5 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
- public override float DefaultFlashlightSize => 350;
+ public override float DefaultFlashlightSize => 325;
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);
@@ -44,7 +44,19 @@ namespace osu.Game.Rulesets.Catch.Mods
: base(modFlashlight)
{
this.playfield = playfield;
- FlashlightSize = new Vector2(0, GetSizeFor(0));
+
+ FlashlightSize = new Vector2(0, GetSize());
+ FlashlightSmoothness = 1.4f;
+ }
+
+ protected override float GetComboScaleFor(int combo)
+ {
+ if (combo >= 200)
+ return 0.770f;
+ if (combo >= 100)
+ return 0.885f;
+
+ return 1.0f;
}
protected override void Update()
@@ -54,9 +66,9 @@ namespace osu.Game.Rulesets.Catch.Mods
FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this);
}
- protected override void OnComboChange(ValueChangedEvent e)
+ protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index ef2936ac94..27f7886d79 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input;
using osu.Game.Beatmaps;
@@ -36,7 +37,9 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader]
private void load()
{
- KeyBindingInputManager.Add(new CatchTouchInputMapper());
+ // With relax mod, input maps directly to x position and left/right buttons are not used.
+ if (!Mods.Any(m => m is ModRelax))
+ KeyBindingInputManager.Add(new CatchTouchInputMapper());
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
index 6e6e83f9cf..0e4f612999 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@@ -30,15 +31,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Cached(typeof(IScrollingInfo))]
private IScrollingInfo scrollingInfo;
+ [Cached]
+ private readonly StageDefinition stage = new StageDefinition(5);
+
protected ManiaPlacementBlueprintTestScene()
{
scrollingInfo = ((ScrollingTestContainer)HitObjectContainer).ScrollingInfo;
- Add(column = new Column(0)
+ Add(column = new Column(0, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- AccentColour = Color4.OrangeRed,
+ AccentColour = { Value = Color4.OrangeRed },
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
index 679a15e8cb..4cadcf138b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
protected ManiaSelectionBlueprintTestScene(int columns)
{
- var stageDefinitions = new List { new StageDefinition { Columns = columns } };
+ var stageDefinitions = new List { new StageDefinition(columns) };
base.Content.Child = scrollingTestContainer = new ScrollingTestContainer(ScrollingDirection.Up)
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index ec96205067..ef140995ec 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
[Cached(typeof(EditorBeatmap))]
- private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition())
+ private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition(2))
{
BeatmapInfo =
{
@@ -56,8 +56,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
Playfield = new ManiaPlayfield(new List
{
- new StageDefinition { Columns = 4 },
- new StageDefinition { Columns = 3 }
+ new StageDefinition(4),
+ new StageDefinition(3)
})
{
Clock = new FramedClock(new StopwatchClock())
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
index e96a186ae4..e082b90d3b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
AddStep("setup compose screen", () =>
{
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ var beatmap = new ManiaBeatmap(new StageDefinition(4))
{
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
};
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
index fcc9e2e6c3..a3985be936 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs
@@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
InternalChildren = new Drawable[]
{
- EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ EditorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }
}),
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs
deleted file mode 100644
index e53deb5269..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/ManiaColumnTypeTest.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System.Collections.Generic;
-using osu.Game.Rulesets.Mania.Beatmaps;
-using NUnit.Framework;
-
-namespace osu.Game.Rulesets.Mania.Tests
-{
- [TestFixture]
- public class ManiaColumnTypeTest
- {
- [TestCase(new[]
- {
- ColumnType.Special
- }, 1)]
- [TestCase(new[]
- {
- ColumnType.Odd,
- ColumnType.Even,
- ColumnType.Even,
- ColumnType.Odd
- }, 4)]
- [TestCase(new[]
- {
- ColumnType.Odd,
- ColumnType.Even,
- ColumnType.Odd,
- ColumnType.Special,
- ColumnType.Odd,
- ColumnType.Even,
- ColumnType.Odd
- }, 7)]
- public void Test(IEnumerable expected, int columns)
- {
- var definition = new StageDefinition
- {
- Columns = columns
- };
- var results = getResults(definition);
- Assert.AreEqual(expected, results);
- }
-
- private IEnumerable getResults(StageDefinition definition)
- {
- for (int i = 0; i < definition.Columns; i++)
- yield return definition.GetTypeOfColumn(i);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
index eb380c07a6..e456659ac4 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaInputTestScene.cs
@@ -3,9 +3,11 @@
#nullable disable
+using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
+using osu.Game.Input.Bindings;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
@@ -37,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
}
- protected override void ReloadMappings()
+ protected override void ReloadMappings(IQueryable realmKeyBindings)
{
KeyBindings = DefaultKeyBindings;
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
index b64006316e..7d1a934456 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyReplayTest.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(ManiaAction.Key8)]
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
{
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 9 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(9));
var frame = new ManiaReplayFrame(0, actions);
var legacyFrame = frame.ToLegacy(beatmap);
@@ -38,8 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(ManiaAction.Key8)]
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
{
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 5 });
- beatmap.Stages.Add(new StageDefinition { Columns = 5 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(5));
+ beatmap.Stages.Add(new StageDefinition(5));
var frame = new ManiaReplayFrame(0, actions);
var legacyFrame = frame.ToLegacy(beatmap);
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs
new file mode 100644
index 0000000000..3bd654e75e
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaSpecialColumnTest.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable disable
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using NUnit.Framework;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ [TestFixture]
+ public class ManiaSpecialColumnTest
+ {
+ [TestCase(new[]
+ {
+ true
+ }, 1)]
+ [TestCase(new[]
+ {
+ false,
+ false,
+ false,
+ false
+ }, 4)]
+ [TestCase(new[]
+ {
+ false,
+ false,
+ false,
+ true,
+ false,
+ false,
+ false
+ }, 7)]
+ public void Test(IEnumerable special, int columns)
+ {
+ var definition = new StageDefinition(columns);
+ var results = getResults(definition);
+ Assert.AreEqual(special, results);
+ }
+
+ private IEnumerable getResults(StageDefinition definition)
+ {
+ for (int i = 0; i < definition.Columns; i++)
+ yield return definition.IsSpecialColumn(i);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
index 7970d5b594..d27a79c41d 100644
--- a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModHoldOff.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
private static ManiaBeatmap createRawBeatmap()
{
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(1));
beatmap.ControlPointInfo.Add(0.0, new TimingControlPoint { BeatLength = 1000 }); // Set BPM to 60
// Add test hit objects
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
index 1a3513d46c..d3e90170b2 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
@@ -24,15 +23,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached]
private readonly Column column;
+ [Cached]
+ private readonly StageDefinition stageDefinition = new StageDefinition(5);
+
public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false)
{
InternalChildren = new[]
{
- this.column = new Column(column)
+ this.column = new Column(column, false)
{
Action = { Value = action },
- AccentColour = Color4.Orange,
- ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd,
Alpha = showColumn ? 1 : 0
},
content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
index fd82041ad8..75175c43d8 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs
@@ -61,7 +61,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
c.Add(CreateHitObject().With(h =>
{
h.HitObject.StartTime = Time.Current + 5000;
- h.AccentColour.Value = Color4.Orange;
}));
})
},
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
index 9f235689b4..2c5535a65f 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaSkinnableTestScene.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
using osu.Game.Tests.Visual;
@@ -24,6 +25,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
+ [Cached]
+ private readonly StageDefinition stage = new StageDefinition(4);
+
protected override Ruleset CreateRulesetForSkinProvider() => new ManiaRuleset();
protected ManiaSkinnableTestScene()
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
index ff557638a9..1bfe55b074 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneBarLine.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
var stageDefinitions = new List
{
- new StageDefinition { Columns = 4 },
+ new StageDefinition(4),
};
SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
deleted file mode 100644
index bbbd7edb7b..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneKeyArea.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Mania.UI.Components;
-using osu.Game.Skinning;
-using osuTK;
-
-namespace osu.Game.Rulesets.Mania.Tests.Skinning
-{
- public class TestSceneKeyArea : ManiaSkinnableTestScene
- {
- [BackgroundDependencyLoader]
- private void load()
- {
- SetContents(_ => new FillFlowContainer
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Size = new Vector2(0.8f),
- Direction = FillDirection.Horizontal,
- Children = new Drawable[]
- {
- new ColumnTestContainer(0, ManiaAction.Key1)
- {
- RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
- {
- RelativeSizeAxes = Axes.Both
- },
- },
- new ColumnTestContainer(1, ManiaAction.Key2)
- {
- RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
- Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
- {
- RelativeSizeAxes = Axes.Both
- },
- },
- }
- });
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
index 62dadbc3dd..9817719c94 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestScenePlayfield.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
stageDefinitions = new List
{
- new StageDefinition { Columns = 2 }
+ new StageDefinition(2)
};
SetContents(_ => new ManiaPlayfield(stageDefinitions));
@@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
stageDefinitions = new List
{
- new StageDefinition { Columns = 2 },
- new StageDefinition { Columns = 2 }
+ new StageDefinition(2),
+ new StageDefinition(2)
};
SetContents(_ => new ManiaPlayfield(stageDefinitions));
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
index f3f1b9416f..07aa0b845f 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStage.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
- Child = new Stage(0, new StageDefinition { Columns = 4 }, ref normalAction, ref specialAction)
+ Child = new Stage(0, new StageDefinition(4), ref normalAction, ref specialAction)
};
});
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
index 687b3a747d..0744d7e2e7 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs
@@ -5,7 +5,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Skinning;
@@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }),
+ SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground),
_ => new DefaultStageBackground())
{
Anchor = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
index 6cbc172755..979c90c802 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs
@@ -5,7 +5,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
@@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
- SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null)
+ SetContents(_ => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
index 21ec85bbe6..3abeb8a5f6 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | - |
// | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(1));
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
var generated = new ManiaAutoGenerator(beatmap).Generate();
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | * |
// | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(1));
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
var generated = new ManiaAutoGenerator(beatmap).Generate();
@@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | - | - |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
beatmap.HitObjects.Add(new Note { StartTime = 1000, Column = 1 });
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | * | * |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000, Column = 1 });
@@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | - | |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
beatmap.HitObjects.Add(new Note { StartTime = 2000, Column = 1 });
@@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | * | |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
beatmap.HitObjects.Add(new HoldNote { StartTime = 2000, Duration = 2000, Column = 1 });
@@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | * | |
// | | |
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
+ var beatmap = new ManiaBeatmap(new StageDefinition(2));
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 });
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
index 2922d18713..83491b6fe9 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneColumn.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
@@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(typeof(IReadOnlyList))]
private IReadOnlyList mods { get; set; } = Array.Empty();
+ [Cached]
+ private readonly StageDefinition stage = new StageDefinition(1);
+
private readonly List columns = new List();
public TestSceneColumn()
@@ -84,12 +88,12 @@ namespace osu.Game.Rulesets.Mania.Tests
private Drawable createColumn(ScrollingDirection direction, ManiaAction action, int index)
{
- var column = new Column(index)
+ var column = new Column(index, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 0.85f,
- AccentColour = Color4.OrangeRed,
+ AccentColour = { Value = Color4.OrangeRed },
Action = { Value = action },
};
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
index 223f8dae44..d273f5cb35 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableManiaHitObject.cs
@@ -4,11 +4,13 @@
#nullable disable
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
@@ -24,6 +26,9 @@ namespace osu.Game.Rulesets.Mania.Tests
private Column column;
+ [Cached]
+ private readonly StageDefinition stage = new StageDefinition(1);
+
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -35,11 +40,11 @@ namespace osu.Game.Rulesets.Mania.Tests
RelativeSizeAxes = Axes.Y,
TimeRange = 2000,
Clock = new FramedClock(clock),
- Child = column = new Column(0)
+ Child = column = new Column(0, false)
{
Action = { Value = ManiaAction.Key1 },
Height = 0.85f,
- AccentColour = Color4.Gray
+ AccentColour = { Value = Color4.Gray },
},
};
});
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index a563dc3106..1f139b5b78 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
AddStep("load player", () =>
{
- Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
+ Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition(4))
{
HitObjects = hitObjects,
BeatmapInfo =
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index cf8947c1ed..6387dac957 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
var specialAction = ManiaAction.Special1;
- var stage = new Stage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
+ var stage = new Stage(0, new StageDefinition(2), ref action, ref specialAction);
stages.Add(stage);
return new ScrollingTestContainer(direction)
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
index e84d02775a..9f2e3d2502 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneTimingBasedNoteColouring.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
const double beat_length = 500;
- var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
+ var beatmap = new ManiaBeatmap(new StageDefinition(1))
{
HitObjects =
{
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 2951076591..0d7b03d830 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
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs b/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs
deleted file mode 100644
index 0114987e3c..0000000000
--- a/osu.Game.Rulesets.Mania/Beatmaps/ColumnType.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-namespace osu.Game.Rulesets.Mania.Beatmaps
-{
- public enum ColumnType
- {
- Even,
- Odd,
- Special
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
index 4879ce6748..b5655a4579 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@@ -60,5 +61,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
},
};
}
+
+ public StageDefinition GetStageForColumnIndex(int column)
+ {
+ foreach (var stage in Stages)
+ {
+ if (column < stage.Columns)
+ return stage;
+
+ column -= stage.Columns;
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(column), "Provided index exceeds all available stages");
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 90cd7f57b5..632b7cdcc7 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -93,10 +93,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap CreateBeatmap()
{
- beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
+ beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns);
if (Dual)
- beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
+ beatmap.Stages.Add(new StageDefinition(TargetColumns));
return beatmap;
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
index 54e2d4686f..898b558eb3 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs
@@ -11,32 +11,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
///
/// Defines properties for each stage in a .
///
- public struct StageDefinition
+ public class StageDefinition
{
///
/// The number of s which this stage contains.
///
- public int Columns;
+ public readonly int Columns;
+
+ public StageDefinition(int columns)
+ {
+ if (columns < 1)
+ throw new ArgumentException("Column count must be above zero.", nameof(columns));
+
+ Columns = columns;
+ }
///
/// Whether the column index is a special column for this stage.
///
/// The 0-based column index.
/// Whether the column is a special column.
- public readonly bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
-
- ///
- /// Get the type of column given a column index.
- ///
- /// The 0-based column index.
- /// The type of the column.
- public readonly ColumnType GetTypeOfColumn(int column)
- {
- if (IsSpecialColumn(column))
- return ColumnType.Special;
-
- int distanceToEdge = Math.Min(column, (Columns - 1) - column);
- return distanceToEdge % 2 == 0 ? ColumnType.Odd : ColumnType.Even;
- }
+ public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2;
}
}
diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
index a925e7c0ac..440dec82af 100644
--- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- scoreAccuracy = customAccuracy;
+ scoreAccuracy = calculateCustomAccuracy();
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed.
@@ -73,6 +73,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
///
/// Accuracy used to weight judgements independently from the score's actual accuracy.
///
- private double customAccuracy => (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
+ private double calculateCustomAccuracy()
+ {
+ if (totalHits == 0)
+ return 0;
+
+ return (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
index ad75afff8e..f438d6497c 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs
@@ -33,5 +33,7 @@ namespace osu.Game.Rulesets.Mania.Edit
}
protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
+
+ protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 061dedb07a..6162184c9a 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -26,6 +26,8 @@ using osu.Game.Rulesets.Mania.Edit.Setup;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Mania.Skinning.Argon;
+using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
@@ -66,6 +68,15 @@ namespace osu.Game.Rulesets.Mania
{
switch (skin)
{
+ case TrianglesSkin:
+ return new ManiaTrianglesSkinTransformer(skin, beatmap);
+
+ case ArgonSkin:
+ return new ManiaArgonSkinTransformer(skin, beatmap);
+
+ case DefaultLegacySkin:
+ return new ManiaClassicSkinTransformer(skin, beatmap);
+
case LegacySkin:
return new ManiaLegacySkinTransformer(skin, beatmap);
}
diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
index 21b362df00..f05edb4677 100644
--- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs
@@ -3,29 +3,19 @@
#nullable disable
-using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Mania.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania
{
public class ManiaSkinComponent : GameplaySkinComponent
{
- ///
- /// The intended for this component.
- /// May be null if the component is not a direct member of a .
- ///
- public readonly StageDefinition? StageDefinition;
-
///
/// Creates a new .
///
/// The component.
- /// The intended for this component. May be null if the component is not a direct member of a .
- public ManiaSkinComponent(ManiaSkinComponents component, StageDefinition? stageDefinition = null)
+ public ManiaSkinComponent(ManiaSkinComponents component)
: base(component)
{
- StageDefinition = stageDefinition;
}
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
index 6eaede2112..947915cdf9 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public ManiaFlashlight(ManiaModFlashlight modFlashlight)
: base(modFlashlight)
{
- FlashlightSize = new Vector2(DrawWidth, GetSizeFor(0));
+ FlashlightSize = new Vector2(DrawWidth, GetSize());
AddLayout(flashlightProperties);
}
@@ -54,9 +54,9 @@ namespace osu.Game.Rulesets.Mania.Mods
}
}
- protected override void OnComboChange(ValueChangedEvent e)
+ protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "RectangularFlashlight";
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index 6020348938..a607ed572d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -54,10 +54,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
- protected override void UpdateInitialTransforms()
- {
- }
-
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 19792086a7..48647f9f5f 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -4,12 +4,14 @@
#nullable disable
using System;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Audio;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private Container tailContainer;
private Container tickContainer;
+ private PausableSkinnableSound slidingSample;
+
///
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
///
@@ -108,6 +112,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
},
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
tailContainer = new Container { RelativeSizeAxes = Axes.Both },
+ slidingSample = new PausableSkinnableSound { Looping = true }
});
maskedContents.AddRange(new[]
@@ -118,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
});
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ isHitting.BindValueChanged(updateSlidingSample, true);
+ }
+
protected override void OnApply()
{
base.OnApply();
@@ -322,5 +334,38 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
HoldStartTime = null;
isHitting.Value = false;
}
+
+ protected override void LoadSamples()
+ {
+ // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
+
+ if (HitObject.SampleControlPoint == null)
+ {
+ throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
+ }
+
+ slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray();
+ }
+
+ public override void StopAllSamples()
+ {
+ base.StopAllSamples();
+ slidingSample?.Stop();
+ }
+
+ private void updateSlidingSample(ValueChangedEvent tracking)
+ {
+ if (tracking.NewValue)
+ slidingSample?.Play();
+ else
+ slidingSample?.Stop();
+ }
+
+ protected override void OnFree()
+ {
+ slidingSample.Samples = null;
+ base.OnFree();
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index d374e935ec..ac646ea427 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -30,20 +30,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public bool UpdateResult() => base.UpdateResult(true);
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- // This hitobject should never expire, so this is just a safe maximum.
- LifetimeEnd = LifetimeStart + 30000;
- }
-
protected override void UpdateHitStateTransforms(ArmedState state)
{
// suppress the base call explicitly.
// the hold note head should never change its visual state on its own due to the "freezing" mechanic
// (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line).
// it will be hidden along with its parenting hold note when required.
+
+ // Set `LifetimeEnd` explicitly to a non-`double.MaxValue` because otherwise this DHO is automatically expired.
+ LifetimeEnd = double.PositiveInfinity;
}
public override bool OnPressed(KeyBindingPressEvent e) => false; // Handled by the hold note
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index bcc10ab7bc..73dc937a00 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -23,10 +23,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected readonly IBindable Direction = new Bindable();
- // Leaving the default (10s) makes hitobjects not appear, as this offset is used for the initial state transforms.
- // Calculated as DrawableManiaRuleset.MAX_TIME_RANGE + some additional allowance for velocity < 1.
- protected override double InitialLifetimeOffset => 30000;
-
[Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; }
@@ -69,22 +65,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Direction.BindValueChanged(OnDirectionChanged, true);
}
- protected override void OnApply()
- {
- base.OnApply();
-
- if (ParentHitObject != null)
- AccentColour.BindTo(ParentHitObject.AccentColour);
- }
-
- protected override void OnFree()
- {
- base.OnFree();
-
- if (ParentHitObject != null)
- AccentColour.UnbindFrom(ParentHitObject.AccentColour);
- }
-
protected virtual void OnDirectionChanged(ValueChangedEvent e)
{
Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.cs
similarity index 50%
rename from osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
rename to osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.cs
index 5bd2d3ab48..598a765d3c 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonColumnBackground.cs
@@ -1,8 +1,6 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -12,26 +10,38 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Mania.UI.Components
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
- public class ColumnBackground : CompositeDrawable, IKeyBindingHandler, IHasAccentColour
+ public class ArgonColumnBackground : CompositeDrawable, IKeyBindingHandler
{
- private readonly IBindable action = new Bindable();
-
- private Box background;
- private Box backgroundOverlay;
-
private readonly IBindable direction = new Bindable();
- [BackgroundDependencyLoader]
- private void load(IBindable action, IScrollingInfo scrollingInfo)
- {
- this.action.BindTo(action);
+ private Color4 brightColour;
+ private Color4 dimColour;
+ private Box background = null!;
+ private Box backgroundOverlay = null!;
+
+ [Resolved]
+ private Column column { get; set; } = null!;
+
+ private Bindable accentColour = null!;
+
+ public ArgonColumnBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ Masking = true;
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
InternalChildren = new[]
{
background = new Box
@@ -49,61 +59,42 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}
};
- direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(dir =>
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
{
- backgroundOverlay.Anchor = backgroundOverlay.Origin = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
- updateColours();
+ background.Colour = colour.NewValue.Darken(3).Opacity(0.8f);
+ brightColour = colour.NewValue.Opacity(0.6f);
+ dimColour = colour.NewValue.Opacity(0);
}, true);
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
}
- protected override void LoadComplete()
+ private void onDirectionChanged(ValueChangedEvent direction)
{
- base.LoadComplete();
- updateColours();
- }
-
- private Color4 accentColour;
-
- public Color4 AccentColour
- {
- get => accentColour;
- set
+ if (direction.NewValue == ScrollingDirection.Up)
{
- if (accentColour == value)
- return;
-
- accentColour = value;
-
- updateColours();
+ backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.TopLeft;
+ backgroundOverlay.Colour = ColourInfo.GradientVertical(brightColour, dimColour);
+ }
+ else
+ {
+ backgroundOverlay.Anchor = backgroundOverlay.Origin = Anchor.BottomLeft;
+ backgroundOverlay.Colour = ColourInfo.GradientVertical(dimColour, brightColour);
}
- }
-
- private void updateColours()
- {
- if (!IsLoaded)
- return;
-
- background.Colour = AccentColour.Darken(5);
-
- var brightPoint = AccentColour.Opacity(0.6f);
- var dimPoint = AccentColour.Opacity(0);
-
- backgroundOverlay.Colour = ColourInfo.GradientVertical(
- direction.Value == ScrollingDirection.Up ? brightPoint : dimPoint,
- direction.Value == ScrollingDirection.Up ? dimPoint : brightPoint);
}
public bool OnPressed(KeyBindingPressEvent e)
{
- if (e.Action == action.Value)
+ if (e.Action == column.Action.Value)
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
return false;
}
public void OnReleased(KeyBindingReleaseEvent e)
{
- if (e.Action == action.Value)
+ if (e.Action == column.Action.Value)
backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
new file mode 100644
index 0000000000..af179d5580
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitExplosion.cs
@@ -0,0 +1,97 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonHitExplosion : CompositeDrawable, IHitExplosion
+ {
+ public override bool RemoveWhenNotAlive => true;
+
+ [Resolved]
+ private Column column { get; set; } = null!;
+
+ private readonly IBindable direction = new Bindable();
+
+ private Container largeFaint = null!;
+
+ private Bindable accentColour = null!;
+
+ public ArgonHitExplosion()
+ {
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.X;
+ Height = ArgonNotePiece.NOTE_HEIGHT;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ InternalChildren = new Drawable[]
+ {
+ largeFaint = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS,
+ Blending = BlendingParameters.Additive,
+ Child = new Box
+ {
+ Colour = Color4.White,
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ largeFaint.Colour = Interpolation.ValueAt(0.8f, colour.NewValue, Color4.White, 0, 1);
+
+ largeFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour.NewValue,
+ Roundness = 40,
+ Radius = 60,
+ };
+ }, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ if (direction.NewValue == ScrollingDirection.Up)
+ {
+ Anchor = Anchor.TopCentre;
+ Y = ArgonNotePiece.NOTE_HEIGHT / 2;
+ }
+ else
+ {
+ Anchor = Anchor.BottomCentre;
+ Y = -ArgonNotePiece.NOTE_HEIGHT / 2;
+ }
+ }
+
+ public void Animate(JudgementResult result)
+ {
+ this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
new file mode 100644
index 0000000000..9e449623d5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHitTarget.cs
@@ -0,0 +1,47 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonHitTarget : CompositeDrawable
+ {
+ private readonly IBindable direction = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = ArgonNotePiece.NOTE_HEIGHT;
+
+ Masking = true;
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS;
+
+ InternalChildren = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.3f,
+ Blending = BlendingParameters.Additive,
+ Colour = Color4.White
+ },
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ Anchor = Origin = direction.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
new file mode 100644
index 0000000000..757190c4ae
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldBodyPiece.cs
@@ -0,0 +1,97 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Skinning.Default;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ ///
+ /// Represents length-wise portion of a hold note.
+ ///
+ public class ArgonHoldBodyPiece : CompositeDrawable, IHoldNoteBody
+ {
+ protected readonly Bindable AccentColour = new Bindable();
+ protected readonly IBindable IsHitting = new Bindable();
+
+ private Drawable background = null!;
+ private Box foreground = null!;
+
+ public ArgonHoldBodyPiece()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ // Without this, the width of the body will be slightly larger than the head/tail.
+ Masking = true;
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS;
+ Blending = BlendingParameters.Additive;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(DrawableHitObject? drawableObject)
+ {
+ InternalChildren = new[]
+ {
+ background = new Box { RelativeSizeAxes = Axes.Both },
+ foreground = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Alpha = 0,
+ },
+ };
+
+ if (drawableObject != null)
+ {
+ var holdNote = (DrawableHoldNote)drawableObject;
+
+ AccentColour.BindTo(holdNote.AccentColour);
+ IsHitting.BindTo(holdNote.IsHitting);
+ }
+
+ AccentColour.BindValueChanged(colour =>
+ {
+ background.Colour = colour.NewValue.Darken(1.2f);
+ foreground.Colour = colour.NewValue.Opacity(0.2f);
+ }, true);
+
+ IsHitting.BindValueChanged(hitting =>
+ {
+ const float animation_length = 50;
+
+ foreground.ClearTransforms();
+
+ if (hitting.NewValue)
+ {
+ // wait for the next sync point
+ double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
+
+ using (foreground.BeginDelayedSequence(synchronisedOffset))
+ {
+ foreground.FadeTo(1, animation_length).Then()
+ .FadeTo(0.5f, animation_length)
+ .Loop();
+ }
+ }
+ else
+ {
+ foreground.FadeOut(animation_length);
+ }
+ });
+ }
+
+ public void Recycle()
+ {
+ foreground.ClearTransforms();
+ foreground.Alpha = 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
new file mode 100644
index 0000000000..e1068c6cd8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
@@ -0,0 +1,91 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ internal class ArgonHoldNoteTailPiece : CompositeDrawable
+ {
+ private readonly IBindable direction = new Bindable();
+ private readonly IBindable accentColour = new Bindable();
+
+ private readonly Box colouredBox;
+ private readonly Box shadow;
+
+ public ArgonHoldNoteTailPiece()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = ArgonNotePiece.NOTE_HEIGHT;
+
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS;
+ Masking = true;
+
+ InternalChildren = new Drawable[]
+ {
+ shadow = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.82f,
+ Masking = true,
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS,
+ Children = new Drawable[]
+ {
+ colouredBox = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ }
+ },
+ new Circle
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = ArgonNotePiece.CORNER_RADIUS * 2,
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ if (drawableObject != null)
+ {
+ accentColour.BindTo(drawableObject.AccentColour);
+ accentColour.BindValueChanged(onAccentChanged, true);
+ }
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
+ ? Anchor.TopCentre
+ : Anchor.BottomCentre;
+ }
+
+ private void onAccentChanged(ValueChangedEvent accent)
+ {
+ colouredBox.Colour = ColourInfo.GradientVertical(
+ accent.NewValue,
+ accent.NewValue.Darken(0.1f)
+ );
+
+ shadow.Colour = accent.NewValue.Darken(0.5f);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
new file mode 100644
index 0000000000..e7dfec256d
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonJudgementPiece.cs
@@ -0,0 +1,193 @@
+// 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.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ {
+ protected readonly HitResult Result;
+
+ protected SpriteText JudgementText { get; private set; } = null!;
+
+ private RingExplosion? ringExplosion;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ public ArgonJudgementPiece(HitResult result)
+ {
+ Result = result;
+ Origin = Anchor.Centre;
+ Y = 160;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ JudgementText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = Result.GetDescription().ToUpperInvariant(),
+ Colour = colours.ForHitResult(Result),
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(10, 0),
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
+ },
+ };
+
+ if (Result.IsHit())
+ {
+ AddInternal(ringExplosion = new RingExplosion(Result)
+ {
+ Colour = colours.ForHitResult(Result),
+ });
+ }
+ }
+
+ ///
+ /// Plays the default animation for this judgement piece.
+ ///
+ ///
+ /// The base implementation only handles fade (for all result types) and misses.
+ /// Individual rulesets are recommended to implement their appropriate hit animations.
+ ///
+ public virtual void PlayAnimation()
+ {
+ switch (Result)
+ {
+ default:
+ JudgementText
+ .ScaleTo(Vector2.One)
+ .ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint);
+ break;
+
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ this.MoveTo(Vector2.Zero);
+ this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ this.RotateTo(0);
+ this.RotateTo(40, 800, Easing.InQuint);
+ break;
+ }
+
+ this.FadeOutFromOne(800);
+
+ ringExplosion?.PlayAnimation();
+ }
+
+ public Drawable? GetAboveHitObjectsProxiedContent() => null;
+
+ private class RingExplosion : CompositeDrawable
+ {
+ private readonly float travel = 52;
+
+ public RingExplosion(HitResult result)
+ {
+ const float thickness = 4;
+
+ const float small_size = 9;
+ const float large_size = 14;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Blending = BlendingParameters.Additive;
+
+ int countSmall = 0;
+ int countLarge = 0;
+
+ switch (result)
+ {
+ case HitResult.Meh:
+ countSmall = 3;
+ travel *= 0.3f;
+ break;
+
+ case HitResult.Ok:
+ case HitResult.Good:
+ countSmall = 4;
+ travel *= 0.6f;
+ break;
+
+ case HitResult.Great:
+ case HitResult.Perfect:
+ countSmall = 4;
+ countLarge = 4;
+ break;
+ }
+
+ for (int i = 0; i < countSmall; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
+
+ for (int i = 0; i < countLarge; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
+ }
+
+ public void PlayAnimation()
+ {
+ foreach (var c in InternalChildren)
+ {
+ const float start_position_ratio = 0.3f;
+
+ float direction = RNG.NextSingle(0, 360);
+ float distance = RNG.NextSingle(travel / 2, travel);
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance * start_position_ratio,
+ MathF.Sin(direction) * distance * start_position_ratio
+ ));
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance,
+ MathF.Sin(direction) * distance
+ ), 600, Easing.OutQuint);
+ }
+
+ this.FadeOutFromOne(1000, Easing.OutQuint);
+ }
+
+ public class RingPiece : CircularContainer
+ {
+ public RingPiece(float thickness = 9)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Masking = true;
+ BorderThickness = thickness;
+ BorderColour = Color4.White;
+
+ Child = new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs
new file mode 100644
index 0000000000..7670c9bdf2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonKeyArea.cs
@@ -0,0 +1,272 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonKeyArea : CompositeDrawable, IKeyBindingHandler
+ {
+ private readonly IBindable direction = new Bindable();
+
+ private Container directionContainer = null!;
+ private Drawable background = null!;
+
+ private Circle hitTargetLine = null!;
+
+ private Container bottomIcon = null!;
+ private CircularContainer topIcon = null!;
+
+ private Bindable accentColour = null!;
+
+ [Resolved]
+ private Column column { get; set; } = null!;
+
+ public ArgonKeyArea()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ const float icon_circle_size = 8;
+ const float icon_spacing = 7;
+ const float icon_vertical_offset = -30;
+
+ InternalChild = directionContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ // Ensure the area is tall enough to put the target line in the correct location.
+ // This is to also allow the main background component to overlap the target line
+ // and avoid an inner corner radius being shown below the target line.
+ Height = Stage.HIT_TARGET_POSITION + ArgonNotePiece.CORNER_RADIUS * 2,
+ Children = new[]
+ {
+ new Container
+ {
+ Masking = true,
+ RelativeSizeAxes = Axes.Both,
+ CornerRadius = ArgonNotePiece.CORNER_RADIUS,
+ Child = background = new Box
+ {
+ Name = "Key gradient",
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ hitTargetLine = new Circle
+ {
+ RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Colour = OsuColour.Gray(196 / 255f),
+ Height = ArgonNotePiece.CORNER_RADIUS * 2,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ },
+ new Container
+ {
+ Name = "Icons",
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Children = new Drawable[]
+ {
+ bottomIcon = new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Y = icon_vertical_offset,
+ Children = new[]
+ {
+ new Circle
+ {
+ Size = new Vector2(icon_circle_size),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ },
+ new Circle
+ {
+ X = -icon_spacing,
+ Y = icon_spacing * 1.2f,
+ Size = new Vector2(icon_circle_size),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ },
+ new Circle
+ {
+ X = icon_spacing,
+ Y = icon_spacing * 1.2f,
+ Size = new Vector2(icon_circle_size),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ },
+ }
+ },
+ topIcon = new CircularContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Y = -icon_vertical_offset,
+ Size = new Vector2(22, 14),
+ Masking = true,
+ BorderThickness = 4,
+ BorderColour = Color4.White,
+ EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow },
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ },
+ }
+ }
+ },
+ }
+ };
+
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ background.Colour = colour.NewValue.Darken(0.2f);
+ bottomIcon.Colour = colour.NewValue;
+ },
+ true);
+
+ // Yes, proxy everything.
+ column.TopLevelContainer.Add(CreateProxy());
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ switch (direction.NewValue)
+ {
+ case ScrollingDirection.Up:
+ directionContainer.Scale = new Vector2(1, -1);
+ directionContainer.Anchor = Anchor.TopLeft;
+ directionContainer.Origin = Anchor.BottomLeft;
+ break;
+
+ case ScrollingDirection.Down:
+ directionContainer.Scale = new Vector2(1, 1);
+ directionContainer.Anchor = Anchor.BottomLeft;
+ directionContainer.Origin = Anchor.BottomLeft;
+ break;
+ }
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Action != column.Action.Value) return false;
+
+ const double lighting_fade_in_duration = 70;
+ Color4 lightingColour = getLightingColour();
+
+ background
+ .FlashColour(accentColour.Value.Lighten(0.8f), 200, Easing.OutQuint)
+ .FadeTo(1, lighting_fade_in_duration, Easing.OutQuint)
+ .Then()
+ .FadeTo(0.8f, 500);
+
+ hitTargetLine.FadeColour(Color4.White, lighting_fade_in_duration, Easing.OutQuint);
+ hitTargetLine.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour.Opacity(0.4f),
+ Radius = 20,
+ }, lighting_fade_in_duration, Easing.OutQuint);
+
+ topIcon.ScaleTo(0.9f, lighting_fade_in_duration, Easing.OutQuint);
+ topIcon.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour.Opacity(0.1f),
+ Radius = 20,
+ }, lighting_fade_in_duration, Easing.OutQuint);
+
+ bottomIcon.FadeColour(Color4.White, lighting_fade_in_duration, Easing.OutQuint);
+
+ foreach (var circle in bottomIcon)
+ {
+ circle.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour.Opacity(0.2f),
+ Radius = 60,
+ }, lighting_fade_in_duration, Easing.OutQuint);
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ if (e.Action != column.Action.Value) return;
+
+ const double lighting_fade_out_duration = 800;
+
+ Color4 lightingColour = getLightingColour().Opacity(0);
+
+ // background fades out faster than lighting elements to give better definition to the player.
+ background.FadeTo(0.3f, 50, Easing.OutQuint)
+ .Then()
+ .FadeOut(lighting_fade_out_duration, Easing.OutQuint);
+
+ topIcon.ScaleTo(1f, 200, Easing.OutQuint);
+ topIcon.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour,
+ Radius = 20,
+ }, lighting_fade_out_duration, Easing.OutQuint);
+
+ hitTargetLine.FadeColour(OsuColour.Gray(196 / 255f), lighting_fade_out_duration, Easing.OutQuint);
+ hitTargetLine.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour,
+ Radius = 25,
+ }, lighting_fade_out_duration, Easing.OutQuint);
+
+ bottomIcon.FadeColour(accentColour.Value, lighting_fade_out_duration, Easing.OutQuint);
+
+ foreach (var circle in bottomIcon)
+ {
+ circle.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = lightingColour,
+ Radius = 30,
+ }, lighting_fade_out_duration, Easing.OutQuint);
+ }
+ }
+
+ private Color4 getLightingColour() => Interpolation.ValueAt(0.2f, accentColour.Value, Color4.White, 0, 1);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
new file mode 100644
index 0000000000..454a6b012b
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonNotePiece.cs
@@ -0,0 +1,110 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ internal class ArgonNotePiece : CompositeDrawable
+ {
+ public const float NOTE_HEIGHT = 42;
+
+ public const float CORNER_RADIUS = 3.4f;
+
+ private readonly IBindable direction = new Bindable();
+ private readonly IBindable accentColour = new Bindable();
+
+ private readonly Box colouredBox;
+ private readonly Box shadow;
+
+ public ArgonNotePiece()
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = NOTE_HEIGHT;
+
+ CornerRadius = CORNER_RADIUS;
+ Masking = true;
+
+ InternalChildren = new Drawable[]
+ {
+ shadow = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.82f,
+ Masking = true,
+ CornerRadius = CORNER_RADIUS,
+ Children = new Drawable[]
+ {
+ colouredBox = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ }
+ },
+ new Circle
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.X,
+ Height = CORNER_RADIUS * 2,
+ },
+ new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 4,
+ Icon = FontAwesome.Solid.AngleDown,
+ Size = new Vector2(20),
+ Scale = new Vector2(1, 0.7f)
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(IScrollingInfo scrollingInfo, DrawableHitObject? drawableObject)
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+
+ if (drawableObject != null)
+ {
+ accentColour.BindTo(drawableObject.AccentColour);
+ accentColour.BindValueChanged(onAccentChanged, true);
+ }
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ colouredBox.Anchor = colouredBox.Origin = direction.NewValue == ScrollingDirection.Up
+ ? Anchor.TopCentre
+ : Anchor.BottomCentre;
+ }
+
+ private void onAccentChanged(ValueChangedEvent accent)
+ {
+ colouredBox.Colour = ColourInfo.GradientVertical(
+ accent.NewValue.Lighten(0.1f),
+ accent.NewValue
+ );
+
+ shadow.Colour = accent.NewValue.Darken(0.5f);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.cs
new file mode 100644
index 0000000000..1881695b14
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonStageBackground.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.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ArgonStageBackground : CompositeDrawable
+ {
+ public ArgonStageBackground()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
new file mode 100644
index 0000000000..ae313e0b91
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
@@ -0,0 +1,141 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Argon
+{
+ public class ManiaArgonSkinTransformer : SkinTransformer
+ {
+ private readonly ManiaBeatmap beatmap;
+
+ public ManiaArgonSkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin)
+ {
+ this.beatmap = (ManiaBeatmap)beatmap;
+ }
+
+ public override Drawable? GetDrawableComponent(ISkinComponent component)
+ {
+ switch (component)
+ {
+ case GameplaySkinComponent resultComponent:
+ return new ArgonJudgementPiece(resultComponent.Component);
+
+ case ManiaSkinComponent maniaComponent:
+ // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
+ switch (maniaComponent.Component)
+ {
+ case ManiaSkinComponents.StageBackground:
+ return new ArgonStageBackground();
+
+ case ManiaSkinComponents.ColumnBackground:
+ return new ArgonColumnBackground();
+
+ case ManiaSkinComponents.HoldNoteBody:
+ return new ArgonHoldBodyPiece();
+
+ case ManiaSkinComponents.HoldNoteTail:
+ return new ArgonHoldNoteTailPiece();
+
+ case ManiaSkinComponents.HoldNoteHead:
+ case ManiaSkinComponents.Note:
+ return new ArgonNotePiece();
+
+ case ManiaSkinComponents.HitTarget:
+ return new ArgonHitTarget();
+
+ case ManiaSkinComponents.KeyArea:
+ return new ArgonKeyArea();
+
+ case ManiaSkinComponents.HitExplosion:
+ return new ArgonHitExplosion();
+ }
+
+ break;
+ }
+
+ return base.GetDrawableComponent(component);
+ }
+
+ public override IBindable? GetConfig(TLookup lookup)
+ {
+ if (lookup is ManiaSkinConfigurationLookup maniaLookup)
+ {
+ int column = maniaLookup.ColumnIndex ?? 0;
+ var stage = beatmap.GetStageForColumnIndex(column);
+
+ switch (maniaLookup.Lookup)
+ {
+ case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
+ return SkinUtils.As(new Bindable(2));
+
+ case LegacyManiaSkinConfigurationLookups.StagePaddingBottom:
+ case LegacyManiaSkinConfigurationLookups.StagePaddingTop:
+ return SkinUtils.As(new Bindable(30));
+
+ case LegacyManiaSkinConfigurationLookups.ColumnWidth:
+ return SkinUtils.As(new Bindable(
+ stage.IsSpecialColumn(column) ? 120 : 60
+ ));
+
+ case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
+
+ Color4 colour;
+
+ const int total_colours = 7;
+
+ if (stage.IsSpecialColumn(column))
+ colour = new Color4(159, 101, 255, 255);
+ else
+ {
+ switch (column % total_colours)
+ {
+ case 0:
+ colour = new Color4(240, 216, 0, 255);
+ break;
+
+ case 1:
+ colour = new Color4(240, 101, 0, 255);
+ break;
+
+ case 2:
+ colour = new Color4(240, 0, 130, 255);
+ break;
+
+ case 3:
+ colour = new Color4(192, 0, 240, 255);
+ break;
+
+ case 4:
+ colour = new Color4(0, 96, 240, 255);
+ break;
+
+ case 5:
+ colour = new Color4(0, 226, 240, 255);
+ break;
+
+ case 6:
+ colour = new Color4(0, 240, 96, 255);
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ return SkinUtils.As(new Bindable(colour));
+ }
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs
new file mode 100644
index 0000000000..eb51179cea
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Default/ManiaTrianglesSkinTransformer.cs
@@ -0,0 +1,49 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Bindables;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Default
+{
+ public class ManiaTrianglesSkinTransformer : SkinTransformer
+ {
+ private readonly ManiaBeatmap beatmap;
+
+ public ManiaTrianglesSkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin)
+ {
+ this.beatmap = (ManiaBeatmap)beatmap;
+ }
+
+ private readonly Color4 colourEven = new Color4(6, 84, 0, 255);
+ private readonly Color4 colourOdd = new Color4(94, 0, 57, 255);
+ private readonly Color4 colourSpecial = new Color4(0, 48, 63, 255);
+
+ public override IBindable? GetConfig(TLookup lookup)
+ {
+ if (lookup is ManiaSkinConfigurationLookup maniaLookup)
+ {
+ switch (maniaLookup.Lookup)
+ {
+ case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
+ int column = maniaLookup.ColumnIndex ?? 0;
+
+ var stage = beatmap.GetStageForColumnIndex(column);
+
+ if (stage.IsSpecialColumn(column))
+ return SkinUtils.As(new Bindable(colourSpecial));
+
+ int distanceToEdge = Math.Min(column, (stage.Columns - 1) - column);
+ return SkinUtils.As(new Bindable(distanceToEdge % 2 == 0 ? colourOdd : colourEven));
+ }
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
index ab953ccfb9..e227c80845 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
@@ -20,6 +21,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[Resolved]
protected Column Column { get; private set; }
+ [Resolved]
+ private StageDefinition stage { get; set; }
+
///
/// The column type identifier to use for texture lookups, in the case of no user-provided configuration.
///
@@ -28,19 +32,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[BackgroundDependencyLoader]
private void load()
{
- switch (Column.ColumnType)
+ if (Column.IsSpecial)
+ FallbackColumnIndex = "S";
+ else
{
- case ColumnType.Special:
- FallbackColumnIndex = "S";
- break;
-
- case ColumnType.Odd:
- FallbackColumnIndex = "1";
- break;
-
- case ColumnType.Even:
- FallbackColumnIndex = "2";
- break;
+ int distanceToEdge = Math.Min(Column.Index, (stage.Columns - 1) - Column.Index);
+ FallbackColumnIndex = distanceToEdge % 2 == 0 ? "1" : "2";
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
index 740ccbfe27..d039551cd7 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs
@@ -18,20 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class LegacyStageBackground : CompositeDrawable
{
- private readonly StageDefinition stageDefinition;
-
private Drawable leftSprite;
private Drawable rightSprite;
private ColumnFlow columnBackgrounds;
- public LegacyStageBackground(StageDefinition stageDefinition)
+ public LegacyStageBackground()
{
- this.stageDefinition = stageDefinition;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
- private void load(ISkinSource skin)
+ private void load(ISkinSource skin, StageDefinition stageDefinition)
{
string leftImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftStageImage)?.Value
?? "mania-stage-left";
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.cs
new file mode 100644
index 0000000000..e57927897c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaClassicSkinTransformer.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.Bindables;
+using osu.Game.Beatmaps;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Skinning.Legacy
+{
+ public class ManiaClassicSkinTransformer : ManiaLegacySkinTransformer
+ {
+ public ManiaClassicSkinTransformer(ISkin skin, IBeatmap beatmap)
+ : base(skin, beatmap)
+ {
+ }
+
+ public override IBindable GetConfig(TLookup lookup)
+ {
+ if (lookup is ManiaSkinConfigurationLookup maniaLookup)
+ {
+ var baseLookup = base.GetConfig(lookup);
+
+ if (baseLookup != null)
+ return baseLookup;
+
+ // default provisioning.
+ switch (maniaLookup.Lookup)
+ {
+ case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
+ return SkinUtils.As(new Bindable(Color4.Black));
+ }
+ }
+
+ return base.GetConfig(lookup);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index dd5baa8150..1d39721a2b 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -20,8 +19,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class ManiaLegacySkinTransformer : LegacySkinTransformer
{
- private readonly ManiaBeatmap beatmap;
-
///
/// Mapping of to their corresponding
/// value.
@@ -60,6 +57,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
///
private readonly Lazy hasKeyTexture;
+ private readonly ManiaBeatmap beatmap;
+
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
: base(skin)
{
@@ -113,8 +112,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return new LegacyHitExplosion();
case ManiaSkinComponents.StageBackground:
- Debug.Assert(maniaComponent.StageDefinition != null);
- return new LegacyStageBackground(maniaComponent.StageDefinition.Value);
+ return new LegacyStageBackground();
case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground();
@@ -151,7 +149,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
public override IBindable GetConfig(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
- return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
+ {
+ return base.GetConfig(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.ColumnIndex));
+ }
return base.GetConfig(lookup);
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
index 4d0c321116..e22bf63049 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigExtensions.cs
@@ -15,9 +15,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
///
/// The skin from which configuration is retrieved.
/// The value to retrieve.
- /// If not null, denotes the index of the column to which the entry applies.
- public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null)
+ /// If not null, denotes the index of the column to which the entry applies.
+ public static IBindable GetManiaSkinConfig(this ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? columnIndex = null)
=> skin.GetConfig(
- new ManiaSkinConfigurationLookup(lookup, index));
+ new ManiaSkinConfigurationLookup(lookup, columnIndex));
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs
index e9005a3da0..59188f02f9 100644
--- a/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/ManiaSkinConfigurationLookup.cs
@@ -16,20 +16,21 @@ namespace osu.Game.Rulesets.Mania.Skinning
public readonly LegacyManiaSkinConfigurationLookups Lookup;
///
- /// The intended index for the configuration.
+ /// The column which is being looked up.
/// May be null if the configuration does not apply to a .
+ /// Note that this is the absolute index across all stages.
///
- public readonly int? TargetColumn;
+ public readonly int? ColumnIndex;
///
/// Creates a new .
///
/// The lookup value.
- /// The intended index for the configuration. May be null if the configuration does not apply to a .
- public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null)
+ /// The intended index for the configuration. May be null if the configuration does not apply to a .
+ public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? columnIndex = null)
{
Lookup = lookup;
- TargetColumn = targetColumn;
+ ColumnIndex = columnIndex;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index deb1b155b5..3d46bdaa7b 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -3,30 +3,30 @@
#nullable disable
-using osuTK.Graphics;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Platform;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
-using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.UI;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
[Cached]
- public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour
+ public class Column : ScrollingPlayfield, IKeyBindingHandler
{
public const float COLUMN_WIDTH = 80;
public const float SPECIAL_COLUMN_WIDTH = 70;
@@ -39,23 +39,46 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable Action = new Bindable();
public readonly ColumnHitObjectArea HitObjectArea;
- internal readonly Container TopLevelContainer;
- private readonly DrawablePool hitExplosionPool;
+ internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
+ private DrawablePool hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
- private readonly GameplaySampleTriggerSource sampleTriggerSource;
+ private GameplaySampleTriggerSource sampleTriggerSource;
- public Column(int index)
+ ///
+ /// Whether this is a special (ie. scratch) column.
+ ///
+ public readonly bool IsSpecial;
+
+ public readonly Bindable AccentColour = new Bindable(Color4.Black);
+
+ public Column(int index, bool isSpecial)
{
Index = index;
+ IsSpecial = isSpecial;
RelativeSizeAxes = Axes.Y;
Width = COLUMN_WIDTH;
+ hitPolicy = new OrderedHitPolicy(HitObjectContainer);
+ HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both };
+ }
+
+ [Resolved]
+ private ISkinSource skin { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host)
+ {
+ SkinnableDrawable keyArea;
+
+ skin.SourceChanged += onSourceChanged;
+ onSourceChanged();
+
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
};
InternalChildren = new[]
@@ -64,17 +87,18 @@ namespace osu.Game.Rulesets.Mania.UI
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
- HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both },
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
+ HitObjectArea,
+ keyArea = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
},
background,
- TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both },
+ TopLevelContainer,
new ColumnTouchInputArea(this)
};
- hitPolicy = new OrderedHitPolicy(HitObjectContainer);
+ applyGameWideClock(background);
+ applyGameWideClock(keyArea);
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
@@ -83,20 +107,38 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool(10, 50);
RegisterPool(10, 50);
RegisterPool(50, 250);
+
+ // Some elements don't handle rewind correctly and fixing them is non-trivial.
+ // In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide
+ // clock so they don't need to worry about rewind.
+ // This only works because they handle OnPressed/OnReleased which results in a correct state while rewinding.
+ //
+ // This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind.
+ void applyGameWideClock(Drawable drawable)
+ {
+ drawable.Clock = host.UpdateThread.Clock;
+ drawable.ProcessCustomClock = false;
+ }
+ }
+
+ private void onSourceChanged()
+ {
+ AccentColour.Value = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, Index)?.Value ?? Color4.Black;
}
protected override void LoadComplete()
{
base.LoadComplete();
-
NewResult += OnNewResult;
}
- public ColumnType ColumnType { get; set; }
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
- public bool IsSpecial => ColumnType == ColumnType.Special;
-
- public Color4 AccentColour { get; set; }
+ if (skin != null)
+ skin.SourceChanged -= onSourceChanged;
+ }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@@ -111,7 +153,7 @@ namespace osu.Game.Rulesets.Mania.UI
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)drawableHitObject;
- maniaObject.AccentColour.Value = AccentColour;
+ maniaObject.AccentColour.BindTo(AccentColour);
maniaObject.CheckHittable = hitPolicy.IsHittable;
}
diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
index 871ec9f1a3..9b3f6d7033 100644
--- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
+++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs
@@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X;
+ Masking = true;
+
InternalChild = columns = new FillFlowContainer
{
RelativeSizeAxes = Axes.Y,
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
index 39d17db6be..3680e7ea0a 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultColumnBackground.cs
@@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
[Resolved]
private Column column { get; set; }
+ private Bindable accentColour;
+
public DefaultColumnBackground()
{
RelativeSizeAxes = Axes.Both;
@@ -55,9 +57,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}
};
- background.Colour = column.AccentColour.Darken(5);
- brightColour = column.AccentColour.Opacity(0.6f);
- dimColour = column.AccentColour.Opacity(0);
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ background.Colour = colour.NewValue.Darken(5);
+ brightColour = colour.NewValue.Opacity(0.6f);
+ dimColour = colour.NewValue.Opacity(0);
+ }, true);
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs
index 53fa86125f..97aa897782 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private Container hitTargetLine;
private Drawable hitTargetBar;
+ private Bindable accentColour;
+
[Resolved]
private Column column { get; set; }
@@ -54,12 +56,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components
},
};
- hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
{
- Type = EdgeEffectType.Glow,
- Radius = 5,
- Colour = column.AccentColour.Opacity(0.5f),
- };
+ hitTargetLine.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 5,
+ Colour = colour.NewValue.Opacity(0.5f),
+ };
+ }, true);
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
index 5a0fab2ff4..600c9feb73 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultKeyArea.cs
@@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private Container keyIcon;
private Drawable gradient;
+ private Bindable accentColour;
+
[Resolved]
private Column column { get; set; }
@@ -75,15 +77,19 @@ namespace osu.Game.Rulesets.Mania.UI.Components
}
};
- keyIcon.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Radius = 5,
- Colour = column.AccentColour.Opacity(0.5f),
- };
-
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
+
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ keyIcon.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 5,
+ Colour = colour.NewValue.Opacity(0.5f),
+ };
+ }, true);
}
private void onDirectionChanged(ValueChangedEvent direction)
diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
index e83cd10d2d..59716ee3e2 100644
--- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -32,6 +32,10 @@ namespace osu.Game.Rulesets.Mania.UI
private CircularContainer largeFaint;
private CircularContainer mainGlow1;
+ private CircularContainer mainGlow2;
+ private CircularContainer mainGlow3;
+
+ private Bindable accentColour;
public DefaultHitExplosion()
{
@@ -48,8 +52,6 @@ namespace osu.Game.Rulesets.Mania.UI
const float roundness = 80;
const float initial_height = 10;
- var colour = Interpolation.ValueAt(0.4f, column.AccentColour, Color4.White, 0, 1);
-
InternalChildren = new Drawable[]
{
largeFaint = new CircularContainer
@@ -61,13 +63,6 @@ namespace osu.Game.Rulesets.Mania.UI
// we want our size to be very small so the glow dominates it.
Size = new Vector2(default_large_faint_size),
Blending = BlendingParameters.Additive,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, column.AccentColour, Color4.White, 0, 1).Opacity(0.3f),
- Roundness = 160,
- Radius = 200,
- },
},
mainGlow1 = new CircularContainer
{
@@ -76,15 +71,8 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.6f, column.AccentColour, Color4.White, 0, 1),
- Roundness = 20,
- Radius = 50,
- },
},
- new CircularContainer
+ mainGlow2 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -93,15 +81,8 @@ namespace osu.Game.Rulesets.Mania.UI
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
Rotation = RNG.NextSingle(-angle_variance, angle_variance),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour,
- Roundness = roundness,
- Radius = 40,
- },
},
- new CircularContainer
+ mainGlow3 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -110,18 +91,44 @@ namespace osu.Game.Rulesets.Mania.UI
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
Rotation = RNG.NextSingle(-angle_variance, angle_variance),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = colour,
- Roundness = roundness,
- Radius = 40,
- },
}
};
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
+
+ accentColour = column.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(colour =>
+ {
+ largeFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.1f, colour.NewValue, Color4.White, 0, 1).Opacity(0.3f),
+ Roundness = 160,
+ Radius = 200,
+ };
+ mainGlow1.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.6f, colour.NewValue, Color4.White, 0, 1),
+ Roundness = 20,
+ Radius = 50,
+ };
+ mainGlow2.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.4f, colour.NewValue, Color4.White, 0, 1),
+ Roundness = roundness,
+ Radius = 40,
+ };
+ mainGlow3.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.4f, colour.NewValue, Color4.White, 0, 1),
+ Roundness = roundness,
+ Radius = 40,
+ };
+ }, true);
}
private void onDirectionChanged(ValueChangedEvent direction)
diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
index 28509d1f4e..a7b94f9f22 100644
--- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
@@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Mania.UI
{
base.PrepareForUse();
+ LifetimeStart = Time.Current;
+
(skinnableExplosion?.Drawable as IHitExplosion)?.Animate(Result);
this.Delay(DURATION).Then().Expire();
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index c578bbb703..1273cb3d32 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -19,7 +21,6 @@ using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -28,6 +29,9 @@ namespace osu.Game.Rulesets.Mania.UI
///
public class Stage : ScrollingPlayfield
{
+ [Cached]
+ public readonly StageDefinition Definition;
+
public const float COLUMN_SPACING = 1;
public const float HIT_TARGET_POSITION = 110;
@@ -40,13 +44,6 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly Drawable barLineContainer;
- private readonly Dictionary columnColours = new Dictionary
- {
- { ColumnType.Even, new Color4(6, 84, 0, 255) },
- { ColumnType.Odd, new Color4(94, 0, 57, 255) },
- { ColumnType.Special, new Color4(0, 48, 63, 255) }
- };
-
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Columns.Any(c => c.ReceivePositionalInputAt(screenSpacePos));
private readonly int firstColumnIndex;
@@ -54,6 +51,7 @@ namespace osu.Game.Rulesets.Mania.UI
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
{
this.firstColumnIndex = firstColumnIndex;
+ Definition = definition;
Name = "Stage";
@@ -75,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.UI
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground())
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground())
{
RelativeSizeAxes = Axes.Both
},
@@ -100,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y,
}
},
- new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null)
+ new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null)
{
RelativeSizeAxes = Axes.Both
},
@@ -118,15 +116,13 @@ namespace osu.Game.Rulesets.Mania.UI
for (int i = 0; i < definition.Columns; i++)
{
- var columnType = definition.GetTypeOfColumn(i);
+ bool isSpecial = definition.IsSpecialColumn(i);
- var column = new Column(firstColumnIndex + i)
+ var column = new Column(firstColumnIndex + i, isSpecial)
{
RelativeSizeAxes = Axes.Both,
Width = 1,
- ColumnType = columnType,
- AccentColour = columnColours[columnType],
- Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ }
+ Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ }
};
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
@@ -135,6 +131,37 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
+ private ISkinSource currentSkin;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ currentSkin = skin;
+
+ skin.SourceChanged += onSkinChanged;
+ onSkinChanged();
+ }
+
+ private void onSkinChanged()
+ {
+ float paddingTop = currentSkin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.StagePaddingTop))?.Value ?? 0;
+ float paddingBottom = currentSkin.GetConfig(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.StagePaddingBottom))?.Value ?? 0;
+
+ Padding = new MarginPadding
+ {
+ Top = paddingTop,
+ Bottom = paddingBottom,
+ };
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (currentSkin != null)
+ currentSkin.SourceChanged -= onSkinChanged;
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
index 51871dd9e5..0601dc6068 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderStreamConversion.cs
@@ -148,6 +148,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
}
+ [Test]
+ public void TestFloatEdgeCaseConversion()
+ {
+ Slider slider = null;
+
+ AddStep("select first slider", () =>
+ {
+ slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
+ EditorClock.Seek(slider.StartTime);
+ EditorBeatmap.SelectedHitObjects.Add(slider);
+ });
+
+ AddStep("change to these specific circumstances", () =>
+ {
+ EditorBeatmap.Difficulty.SliderMultiplier = 1;
+ var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(slider.StartTime);
+ timingPoint.BeatLength = 352.941176470588;
+ slider.Path.ControlPoints[^1].Position = new Vector2(-110, 16);
+ slider.Path.ExpectedDistance.Value = 100;
+ });
+
+ convertToStream();
+
+ AddAssert("stream created", () => streamCreatedFor(slider,
+ (time: 0, pathPosition: 0),
+ (time: 0.25, pathPosition: 0.25),
+ (time: 0.5, pathPosition: 0.5),
+ (time: 0.75, pathPosition: 0.75),
+ (time: 1, pathPosition: 1)));
+ }
+
private bool streamCreatedFor(Slider slider, params (double time, double pathPosition)[] expectedCircles)
{
if (EditorBeatmap.HitObjects.Contains(slider))
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png
new file mode 100644
index 0000000000..b1380a47a4
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/cursor-smoke@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png
new file mode 100644
index 0000000000..5f7beae4e9
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
new file mode 100644
index 0000000000..1cb64b71fc
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
@@ -0,0 +1,136 @@
+// 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 NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Framework.Input.States;
+using osu.Framework.Logging;
+using osu.Framework.Testing.Input;
+using osu.Game.Rulesets.Osu.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneSmoke : OsuSkinnableTestScene
+ {
+ [Test]
+ public void TestSmoking()
+ {
+ addStep("Create short smoke", 2_000);
+ addStep("Create medium smoke", 5_000);
+ addStep("Create long smoke", 10_000);
+ }
+
+ private void addStep(string stepName, double duration)
+ {
+ var smokeContainers = new List();
+
+ AddStep(stepName, () =>
+ {
+ smokeContainers.Clear();
+ SetContents(_ =>
+ {
+ smokeContainers.Add(new TestSmokeContainer
+ {
+ Duration = duration,
+ RelativeSizeAxes = Axes.Both
+ });
+
+ return new SmokingInputManager
+ {
+ Duration = duration,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.95f),
+ Child = smokeContainers[^1],
+ };
+ });
+ });
+
+ AddUntilStep("Until skinnable expires", () =>
+ {
+ if (smokeContainers.Count == 0)
+ return false;
+
+ Logger.Log("How many: " + smokeContainers.Count);
+
+ foreach (var smokeContainer in smokeContainers)
+ {
+ if (smokeContainer.Children.Count != 0)
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ private class SmokingInputManager : ManualInputManager
+ {
+ public double Duration { get; init; }
+
+ private double? startTime;
+
+ public SmokingInputManager()
+ {
+ UseParentInput = false;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ MoveMouseTo(ToScreenSpace(DrawSize / 2));
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ const float spin_angle = 4 * MathF.PI;
+
+ startTime ??= Time.Current;
+
+ float fraction = (float)((Time.Current - startTime) / Duration);
+
+ float angle = fraction * spin_angle;
+ float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2;
+
+ Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2;
+ MoveMouseTo(ToScreenSpace(pos));
+ }
+ }
+
+ private class TestSmokeContainer : SmokeContainer
+ {
+ public double Duration { get; init; }
+
+ private bool isPressing;
+ private bool isFinished;
+
+ private double? startTime;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ startTime ??= Time.Current + 0.1;
+
+ if (!isPressing && !isFinished && Time.Current > startTime)
+ {
+ OnPressed(new KeyBindingPressEvent(new InputState(), OsuAction.Smoke));
+ isPressing = true;
+ isFinished = false;
+ }
+
+ if (isPressing && Time.Current > startTime + Duration)
+ {
+ OnReleased(new KeyBindingReleaseEvent(new InputState(), OsuAction.Smoke));
+ isPressing = false;
+ isFinished = true;
+ }
+ }
+ }
+ }
+}
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 c2973644cf..1eb1c85d93 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
@@ -1,10 +1,10 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 7c289b5b05..265a1d21b1 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount();
double pathPosition = positionWithRepeats - (int)positionWithRepeats;
// every second span is in the reverse direction - need to reverse the path position.
- if (Precision.AlmostBigger(positionWithRepeats % 2, 1))
+ if (positionWithRepeats % 2 >= 1)
pathPosition = 1 - pathPosition;
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index 79f5eed139..1a86901d9c 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
- public override float DefaultFlashlightSize => 180;
+ public override float DefaultFlashlightSize => 200;
private OsuFlashlight flashlight = null!;
@@ -62,7 +62,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
followDelay = modFlashlight.FollowDelay.Value;
- FlashlightSize = new Vector2(0, GetSizeFor(0));
+ FlashlightSize = new Vector2(0, GetSize());
+ FlashlightSmoothness = 1.4f;
}
public void OnSliderTrackingChange(ValueChangedEvent e)
@@ -82,9 +83,9 @@ namespace osu.Game.Rulesets.Osu.Mods
return base.OnMouseMove(e);
}
- protected override void OnComboChange(ValueChangedEvent e)
+ protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index fac1cbfd47..753de6231a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -20,7 +20,9 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
+
+ public override Type[] IncompatibleMods =>
+ base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
///
/// How early before a hitobject's start time to trigger a hit.
@@ -51,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
return;
}
- osuInputManager.AllowUserPresses = false;
+ osuInputManager.AllowGameplayInputs = false;
}
public void Update(Playfield playfield)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index c5992b359d..23db29b9a6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -204,12 +204,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut();
- // in the case of an early state change, the fade should be expedited to the current point in time.
- if (HitStateUpdateTime < HitObject.StartTime)
- ApproachCircle.FadeOut(50);
-
switch (state)
{
+ default:
+ ApproachCircle.FadeOut();
+ break;
+
case ArmedState.Idle:
HitArea.HitAction = null;
break;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 6f4ca30bd0..d9d0d28477 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -11,7 +11,9 @@ using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Scoring;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -64,6 +66,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
}
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ // Dim should only be applied at a top level, as it will be implicitly applied to nested objects.
+ if (ParentHitObject == null)
+ {
+ // Of note, no one noticed this was missing for years, but it definitely feels like it should still exist.
+ // For now this is applied across all skins, and matches stable.
+ // For simplicity, dim colour is applied to the DrawableHitObject itself.
+ // We may need to make a nested container setup if this even causes a usage conflict (ie. with a mod).
+ this.FadeColour(new Color4(195, 195, 195, 255));
+ using (BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
+ this.FadeColour(Color4.White, 100);
+ }
+ }
+
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
private OsuInputManager osuActionInputManager;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
index 6bfb4e8aae..a2fe623897 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
@@ -186,17 +186,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Vector2? lastPosition;
+ private bool rewinding;
+
public void UpdateProgress(double completionProgress)
{
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
+ if (Clock.ElapsedFrameTime != 0)
+ rewinding = Clock.ElapsedFrameTime < 0;
+
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
if (diff.LengthFast < 0.01f)
return;
- ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
+ ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0);
lastPosition = Position;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index e3c1b1e168..6c2be8a49a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -34,21 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public override IList AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray();
- public IList CreateSlidingSamples()
- {
- var slidingSamples = new List();
-
- var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
- if (normalSample != null)
- slidingSamples.Add(normalSample.With("sliderslide"));
-
- var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
- if (whistleSample != null)
- slidingSamples.Add(whistleSample.With("sliderwhistle"));
-
- return slidingSamples;
- }
-
private readonly Cached endPositionCache = new Cached();
public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1);
diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs
index 12256e93d0..1e59e19246 100644
--- a/osu.Game.Rulesets.Osu/OsuInputManager.cs
+++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs
@@ -5,10 +5,12 @@
using System.Collections.Generic;
using System.ComponentModel;
+using System.Linq;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events;
+using osu.Game.Input.Bindings;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@@ -17,9 +19,16 @@ namespace osu.Game.Rulesets.Osu
{
public IEnumerable PressedActions => KeyBindingContainer.PressedActions;
- public bool AllowUserPresses
+ ///
+ /// Whether gameplay input buttons should be allowed.
+ /// Defaults to true, generally used for mods like Relax which turn off main inputs.
+ ///
+ ///
+ /// Of note, auxiliary inputs like the "smoke" key are left usable.
+ ///
+ public bool AllowGameplayInputs
{
- set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value;
+ set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value;
}
///
@@ -58,18 +67,36 @@ namespace osu.Game.Rulesets.Osu
private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
- public bool AllowUserPresses = true;
+ private bool allowGameplayInputs = true;
+
+ ///
+ /// Whether gameplay input buttons should be allowed.
+ /// Defaults to true, generally used for mods like Relax which turn off main inputs.
+ ///
+ ///
+ /// Of note, auxiliary inputs like the "smoke" key are left usable.
+ ///
+ public bool AllowGameplayInputs
+ {
+ get => allowGameplayInputs;
+ set
+ {
+ allowGameplayInputs = value;
+ ReloadMappings();
+ }
+ }
public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
: base(ruleset, variant, unique)
{
}
- protected override bool Handle(UIEvent e)
+ protected override void ReloadMappings(IQueryable realmKeyBindings)
{
- if (!AllowUserPresses) return false;
+ base.ReloadMappings(realmKeyBindings);
- return base.Handle(e);
+ if (!AllowGameplayInputs)
+ KeyBindings = KeyBindings.Where(b => b.GetAction() == OsuAction.Smoke).ToList();
}
}
}
@@ -80,6 +107,9 @@ namespace osu.Game.Rulesets.Osu
LeftButton,
[Description("Right button")]
- RightButton
+ RightButton,
+
+ [Description("Smoke")]
+ Smoke,
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 3f5e728651..e823053be9 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Osu
{
new KeyBinding(InputKey.Z, OsuAction.LeftButton),
new KeyBinding(InputKey.X, OsuAction.RightButton),
+ new KeyBinding(InputKey.C, OsuAction.Smoke),
new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton),
new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
};
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index fcf079b6aa..4248cce55a 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu
SliderBall,
SliderBody,
SpinnerBody,
+ CursorSmoke,
ApproachCircle,
}
}
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
index 85060261fe..8082c5aef4 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs
@@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Position = currentFrame.Position;
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton);
+ if (currentFrame.Smoke) Actions.Add(OsuAction.Smoke);
}
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
@@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Replays
state |= ReplayButtonState.Left1;
if (Actions.Contains(OsuAction.RightButton))
state |= ReplayButtonState.Right1;
+ if (Actions.Contains(OsuAction.Smoke))
+ state |= ReplayButtonState.Smoke;
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
}
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
index 05fbac625e..6f55e1790f 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
@@ -1,20 +1,23 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuHitWindows : HitWindows
{
+ ///
+ /// osu! ruleset has a fixed miss window regardless of difficulty settings.
+ ///
+ public const double MISS_WINDOW = 400;
+
private static readonly DifficultyRange[] osu_ranges =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Ok, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100),
- new DifficultyRange(HitResult.Miss, 400, 400, 400),
+ new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW),
};
public override bool IsHitResultAllowed(HitResult result)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
index 7bc6723afb..bf507db50c 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
return new ArgonJudgementPiece(resultComponent.Component);
case OsuSkinComponent osuComponent:
+ // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
switch (osuComponent.Component)
{
case OsuSkinComponents.HitCircle:
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs
new file mode 100644
index 0000000000..27a2dc3960
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSmokeSegment.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Textures;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Default
+{
+ public class DefaultSmokeSegment : SmokeSegment
+ {
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ // ISkinSource doesn't currently fallback to global textures.
+ // We might want to change this in the future if the intention is to allow the user to skin this as per legacy skins.
+ Texture = textures.Get("Gameplay/osu/cursor-smoke");
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
new file mode 100644
index 0000000000..c9c7e86e86
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+{
+ public class LegacySmokeSegment : SmokeSegment
+ {
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ base.LoadComplete();
+
+ Texture = skin.GetTexture("cursor-smoke");
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
index 885a2c12fb..b778bc21d1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs
@@ -106,6 +106,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
+ case OsuSkinComponents.CursorSmoke:
+ if (GetTexture("cursor-smoke") != null)
+ return new LegacySmokeSegment();
+
+ return null;
+
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null;
diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
new file mode 100644
index 0000000000..6c998e244c
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
@@ -0,0 +1,366 @@
+// 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;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Rendering.Vertices;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Utils;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
+ {
+ private const int max_point_count = 18_000;
+
+ // fade anim values
+ private const double initial_fade_out_duration = 4000;
+
+ private const double re_fade_in_speed = 3;
+ private const double re_fade_in_duration = 50;
+
+ private const double final_fade_out_speed = 2;
+ private const double final_fade_out_duration = 8000;
+
+ private const float initial_alpha = 0.6f;
+ private const float re_fade_in_alpha = 1f;
+
+ private readonly int rotationSeed = RNG.Next();
+
+ // scale anim values
+ private const double scale_duration = 1200;
+
+ private const float initial_scale = 0.65f;
+ private const float final_scale = 1f;
+
+ // rotation anim values
+ private const double rotation_duration = 500;
+
+ private const float max_rotation = 0.25f;
+
+ public IShader? TextureShader { get; private set; }
+ public IShader? RoundedTextureShader { get; private set; }
+
+ protected Texture? Texture { get; set; }
+
+ private float radius => Texture?.DisplayWidth * 0.165f ?? 3;
+
+ protected readonly List SmokePoints = new List();
+
+ private float pointInterval => radius * 7f / 8;
+
+ private double smokeStartTime { get; set; } = double.MinValue;
+
+ private double smokeEndTime { get; set; } = double.MaxValue;
+
+ private float totalDistance;
+ private Vector2? lastPosition;
+
+ [BackgroundDependencyLoader]
+ private void load(ShaderManager shaders)
+ {
+ RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
+ TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ RelativeSizeAxes = Axes.Both;
+
+ LifetimeStart = smokeStartTime = Time.Current;
+
+ totalDistance = pointInterval;
+ }
+
+ private Vector2 nextPointDirection()
+ {
+ float angle = RNG.NextSingle(0, 2 * MathF.PI);
+ return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
+ }
+
+ public void AddPosition(Vector2 position, double time)
+ {
+ lastPosition ??= position;
+
+ float delta = (position - (Vector2)lastPosition).LengthFast;
+ totalDistance += delta;
+ int count = (int)(totalDistance / pointInterval);
+
+ if (count > 0)
+ {
+ Vector2 increment = position - (Vector2)lastPosition;
+ increment.NormalizeFast();
+
+ Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
+ increment *= pointInterval;
+
+ if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
+ {
+ int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
+ SmokePoints.RemoveRange(index, SmokePoints.Count - index);
+ }
+
+ totalDistance %= pointInterval;
+
+ for (int i = 0; i < count; i++)
+ {
+ SmokePoints.Add(new SmokePoint
+ {
+ Position = pointPos,
+ Time = time,
+ Direction = nextPointDirection(),
+ });
+
+ pointPos += increment;
+ }
+
+ Invalidate(Invalidation.DrawNode);
+ }
+
+ lastPosition = position;
+
+ if (SmokePoints.Count >= max_point_count)
+ FinishDrawing(time);
+ }
+
+ public void FinishDrawing(double time)
+ {
+ smokeEndTime = time;
+
+ double initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, smokeEndTime - smokeStartTime);
+ LifetimeEnd = smokeEndTime + final_fade_out_duration + initialFadeOutDurationTrunc / re_fade_in_speed + initialFadeOutDurationTrunc / final_fade_out_speed;
+ }
+
+ protected override DrawNode CreateDrawNode() => new SmokeDrawNode(this);
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Invalidate(Invalidation.DrawNode);
+ }
+
+ protected struct SmokePoint
+ {
+ public Vector2 Position;
+ public double Time;
+ public Vector2 Direction;
+
+ public struct UpperBoundComparer : IComparer
+ {
+ public int Compare(SmokePoint x, SmokePoint target)
+ {
+ // By returning -1 when the target value is equal to x, guarantees that the
+ // element at BinarySearch's returned index will always be the first element
+ // larger. Since 0 is never returned, the target is never "found", so the return
+ // value will be the index's complement.
+
+ return x.Time > target.Time ? 1 : -1;
+ }
+ }
+ }
+
+ protected class SmokeDrawNode : TexturedShaderDrawNode
+ {
+ protected new SmokeSegment Source => (SmokeSegment)base.Source;
+
+ protected double SmokeStartTime { get; private set; }
+ protected double SmokeEndTime { get; private set; }
+ protected double CurrentTime { get; private set; }
+
+ private readonly List points = new List();
+ private IVertexBatch? quadBatch;
+ private float radius;
+ private Vector2 drawSize;
+ private Texture? texture;
+
+ // anim calculation vars (color, scale, direction)
+ private double initialFadeOutDurationTrunc;
+ private double firstVisiblePointTime;
+
+ private double initialFadeOutTime;
+ private double reFadeInTime;
+ private double finalFadeOutTime;
+
+ private Random rotationRNG = new Random();
+
+ public SmokeDrawNode(ITexturedShaderDrawable source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ points.Clear();
+ points.AddRange(Source.SmokePoints);
+
+ radius = Source.radius;
+ drawSize = Source.DrawSize;
+ texture = Source.Texture;
+
+ SmokeStartTime = Source.smokeStartTime;
+ SmokeEndTime = Source.smokeEndTime;
+ CurrentTime = Source.Clock.CurrentTime;
+
+ rotationRNG = new Random(Source.rotationSeed);
+
+ initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
+ firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
+
+ initialFadeOutTime = CurrentTime;
+ reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
+ finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
+ }
+
+ public sealed override void Draw(IRenderer renderer)
+ {
+ base.Draw(renderer);
+
+ if (points.Count == 0)
+ return;
+
+ quadBatch ??= renderer.CreateQuadBatch(max_point_count / 10, 10);
+ texture ??= renderer.WhitePixel;
+ RectangleF textureRect = texture.GetTextureRect();
+
+ var shader = GetAppropriateShader(renderer);
+
+ renderer.SetBlend(BlendingParameters.Additive);
+ renderer.PushLocalMatrix(DrawInfo.Matrix);
+
+ shader.Bind();
+ texture.Bind();
+
+ foreach (var point in points)
+ drawPointQuad(point, textureRect);
+
+ shader.Unbind();
+ renderer.PopLocalMatrix();
+ }
+
+ protected Color4 ColourAtPosition(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour
+ ? ((SRGBColour)DrawColourInfo.Colour).Linear
+ : DrawColourInfo.Colour.Interpolate(Vector2.Divide(localPos, drawSize)).Linear;
+
+ protected virtual Color4 PointColour(SmokePoint point)
+ {
+ var color = Color4.White;
+
+ double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time;
+
+ if (timeDoingInitialFadeOut > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
+ color.A = (1 - fraction) * initial_alpha;
+ }
+
+ if (color.A > 0)
+ {
+ double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
+ double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
+
+ if (timeDoingFinalFadeOut > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
+ fraction = MathF.Pow(fraction, 5);
+ color.A = (1 - fraction) * re_fade_in_alpha;
+ }
+ else if (timeDoingReFadeIn > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
+ }
+ }
+
+ return color;
+ }
+
+ protected virtual float PointScale(SmokePoint point)
+ {
+ double timeDoingScale = CurrentTime - point.Time;
+ float fraction = Math.Clamp((float)(timeDoingScale / scale_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ return fraction * (final_scale - initial_scale) + initial_scale;
+ }
+
+ protected virtual Vector2 PointDirection(SmokePoint point)
+ {
+ float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
+ float finalAngle = initialAngle + nextRotation();
+
+ double timeDoingRotation = CurrentTime - point.Time;
+ float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ float angle = fraction * (finalAngle - initialAngle) + initialAngle;
+
+ return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
+ }
+
+ private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1);
+
+ private void drawPointQuad(SmokePoint point, RectangleF textureRect)
+ {
+ Debug.Assert(quadBatch != null);
+
+ var colour = PointColour(point);
+ float scale = PointScale(point);
+ var dir = PointDirection(point);
+ var ortho = dir.PerpendicularLeft;
+
+ if (colour.A == 0 || scale == 0)
+ return;
+
+ var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
+ var localTopRight = point.Position + (radius * scale * (-ortho + dir));
+ var localBotLeft = point.Position + (radius * scale * (ortho - dir));
+ var localBotRight = point.Position + (radius * scale * (ortho + dir));
+
+ quadBatch.Add(new TexturedVertex2D
+ {
+ Position = localTopLeft,
+ TexturePosition = textureRect.TopLeft,
+ Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
+ });
+ quadBatch.Add(new TexturedVertex2D
+ {
+ Position = localTopRight,
+ TexturePosition = textureRect.TopRight,
+ Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
+ });
+ quadBatch.Add(new TexturedVertex2D
+ {
+ Position = localBotRight,
+ TexturePosition = textureRect.BottomRight,
+ Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
+ });
+ quadBatch.Add(new TexturedVertex2D
+ {
+ Position = localBotLeft,
+ TexturePosition = textureRect.BottomLeft,
+ Colour = Color4Extensions.Multiply(ColourAtPosition(localBotLeft), colour),
+ });
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ quadBatch?.Dispose();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index fc2ba8ea2f..2e67e91460 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
+ new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both },
diff --git a/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs b/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
new file mode 100644
index 0000000000..beba834e88
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
@@ -0,0 +1,77 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Osu.Skinning;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.UI
+{
+ ///
+ /// Manages smoke trails generated from user input.
+ ///
+ public class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler
+ {
+ private SmokeSkinnableDrawable? currentSegmentSkinnable;
+
+ private Vector2 lastMousePosition;
+
+ public override bool ReceivePositionalInputAt(Vector2 _) => true;
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (e.Action == OsuAction.Smoke)
+ {
+ AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment()));
+
+ // Add initial position immediately.
+ addPosition();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ if (e.Action == OsuAction.Smoke)
+ {
+ if (currentSegmentSkinnable?.Drawable is SmokeSegment segment)
+ {
+ segment.FinishDrawing(Time.Current);
+ currentSegmentSkinnable = null;
+ }
+ }
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ lastMousePosition = e.MousePosition;
+ addPosition();
+
+ return base.OnMouseMove(e);
+ }
+
+ private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current);
+
+ private class SmokeSkinnableDrawable : SkinnableDrawable
+ {
+ public override bool RemoveWhenNotAlive => true;
+
+ public override double LifetimeStart => Drawable.LifetimeStart;
+ public override double LifetimeEnd => Drawable.LifetimeEnd;
+
+ public SmokeSkinnableDrawable(ISkinComponent component, Func? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
+ : base(component, defaultImplementation, confineMode)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs
new file mode 100644
index 0000000000..095fddc33f
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineGeneration.cs
@@ -0,0 +1,87 @@
+// 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.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneBarLineGeneration : OsuTestScene
+ {
+ [Test]
+ public void TestCloseBarLineGeneration()
+ {
+ const double start_time = 1000;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new Hit
+ {
+ Type = HitType.Centre,
+ StartTime = start_time
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new TaikoRuleset().RulesetInfo
+ },
+ };
+
+ beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint());
+ beatmap.ControlPointInfo.Add(start_time + 1, new TimingControlPoint());
+
+ var barlines = new BarLineGenerator(beatmap).BarLines;
+
+ AddAssert("first barline generated", () => barlines.Any(b => b.StartTime == start_time));
+ AddAssert("second barline generated", () => barlines.Any(b => b.StartTime == start_time + 1));
+ }
+
+ [Test]
+ public void TestOmitBarLineEffectPoint()
+ {
+ const double start_time = 1000;
+ const double beat_length = 500;
+
+ const int time_signature_numerator = 4;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new Hit
+ {
+ Type = HitType.Centre,
+ StartTime = start_time
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new TaikoRuleset().RulesetInfo
+ },
+ };
+
+ beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint
+ {
+ BeatLength = beat_length,
+ TimeSignature = new TimeSignature(time_signature_numerator)
+ });
+
+ beatmap.ControlPointInfo.Add(start_time, new EffectControlPoint { OmitFirstBarLine = true });
+
+ var barlines = new BarLineGenerator(beatmap).BarLines;
+
+ AddAssert("first barline ommited", () => barlines.All(b => b.StartTime != start_time));
+ AddAssert("second barline generated", () => barlines.Any(b => b.StartTime == start_time + (beat_length * time_signature_numerator)));
+ }
+ }
+}
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 e8eaff4368..38e61f5624 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
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index 95a1e8bc66..dc7bad2f75 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countOk;
private int countMeh;
private int countMiss;
+ private double accuracy;
private double effectiveMissCount;
@@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
+ accuracy = customAccuracy;
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
if (totalSuccessfulHits > 0)
@@ -87,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModFlashlight))
difficultyValue *= 1.050 * lengthBonus;
- return difficultyValue * Math.Pow(score.Accuracy, 2.0);
+ return difficultyValue * Math.Pow(accuracy, 2.0);
}
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0)
return 0;
- double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
+ double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;
@@ -110,5 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
+
+ private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index 1caacdd1d7..98f954ad29 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
- public override float DefaultFlashlightSize => 250;
+ public override float DefaultFlashlightSize => 200;
protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, playfield);
@@ -46,20 +46,22 @@ namespace osu.Game.Rulesets.Taiko.Mods
: base(modFlashlight)
{
this.taikoPlayfield = taikoPlayfield;
- FlashlightSize = getSizeFor(0);
+
+ FlashlightSize = adjustSize(GetSize());
+ FlashlightSmoothness = 1.4f;
AddLayout(flashlightProperties);
}
- private Vector2 getSizeFor(int combo)
+ private Vector2 adjustSize(float size)
{
// Preserve flashlight size through the playfield's aspect adjustment.
- return new Vector2(0, GetSizeFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
+ return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
}
- protected override void OnComboChange(ValueChangedEvent e)
+ protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), getSizeFor(e.NewValue), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), adjustSize(size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
@@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
ClearTransforms(targetMember: nameof(FlashlightSize));
- FlashlightSize = getSizeFor(Combo.Value);
+ FlashlightSize = adjustSize(Combo.Value);
flashlightProperties.Validate();
}
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 3f20f843a7..e7590df3e0 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
-using System.Threading.Tasks;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -78,7 +77,7 @@ namespace osu.Game.Tests.Online
}
};
- beatmaps.AllowImport = new TaskCompletionSource();
+ beatmaps.AllowImport.Reset();
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
@@ -132,7 +131,7 @@ namespace osu.Game.Tests.Online
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
- AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
+ AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
@@ -141,7 +140,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestTrackerRespectsSoftDeleting()
{
- AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
+ AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
@@ -155,7 +154,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestTrackerRespectsChecksum()
{
- AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
+ AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable);
@@ -202,7 +201,7 @@ namespace osu.Game.Tests.Online
private class TestBeatmapManager : BeatmapManager
{
- public TaskCompletionSource AllowImport = new TaskCompletionSource();
+ public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim();
public Live CurrentImport { get; private set; }
@@ -229,7 +228,9 @@ namespace osu.Game.Tests.Online
public override Live ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
{
- testBeatmapManager.AllowImport.Task.WaitSafely();
+ if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
+ throw new TimeoutException("Timeout waiting for import to be allowed.");
+
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
index 7e0981ce69..54ad4e25e4 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs
@@ -29,16 +29,18 @@ namespace osu.Game.Tests.Visual.Editing
private TimelineBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType().First();
+ private Vector2 getPosition(HitObject hitObject) =>
+ blueprintContainer.SelectionBlueprints.First(s => s.Item == hitObject).ScreenSpaceDrawQuad.Centre;
+
+ private Vector2 getMiddlePosition(HitObject hitObject1, HitObject hitObject2) =>
+ (getPosition(hitObject1) + getPosition(hitObject2)) / 2;
+
private void moveMouseToObject(Func targetFunc)
{
AddStep("move mouse to object", () =>
{
- var pos = blueprintContainer.SelectionBlueprints
- .First(s => s.Item == targetFunc())
- .ChildrenOfType()
- .First().ScreenSpaceDrawQuad.Centre;
-
- InputManager.MoveMouseTo(pos);
+ var hitObject = targetFunc();
+ InputManager.MoveMouseTo(getPosition(hitObject));
});
}
@@ -262,6 +264,56 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
+ [Test]
+ public void TestBasicDragSelection()
+ {
+ var addedObjects = new[]
+ {
+ new HitCircle { StartTime = 0 },
+ new HitCircle { StartTime = 500, Position = new Vector2(100) },
+ new HitCircle { StartTime = 1000, Position = new Vector2(200) },
+ new HitCircle { StartTime = 1500, Position = new Vector2(300) },
+ };
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
+
+ AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1])));
+ AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left));
+
+ AddStep("drag to select", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[2], addedObjects[3])));
+ assertSelectionIs(new[] { addedObjects[1], addedObjects[2] });
+
+ AddStep("drag to deselect", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[1], addedObjects[2])));
+ assertSelectionIs(new[] { addedObjects[1] });
+
+ AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left));
+ assertSelectionIs(new[] { addedObjects[1] });
+ }
+
+ [Test]
+ public void TestFastDragSelection()
+ {
+ var addedObjects = new[]
+ {
+ new HitCircle { StartTime = 0 },
+ new HitCircle { StartTime = 500 },
+ new HitCircle { StartTime = 20000, Position = new Vector2(100) },
+ new HitCircle { StartTime = 31000, Position = new Vector2(200) },
+ new HitCircle { StartTime = 60000, Position = new Vector2(300) },
+ };
+
+ AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
+
+ AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1])));
+ AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left));
+ AddStep("start drag", () => InputManager.MoveMouseTo(getPosition(addedObjects[1])));
+
+ AddStep("jump editor clock", () => EditorClock.Seek(30000));
+ AddStep("jump editor clock", () => EditorClock.Seek(60000));
+ AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
+ assertSelectionIs(addedObjects.Skip(1));
+ AddAssert("all blueprints are present", () => blueprintContainer.SelectionBlueprints.Count == EditorBeatmap.SelectedHitObjects.Count);
+ }
+
private void assertSelectionIs(IEnumerable hitObjects)
=> AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects));
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
index 0a32513834..bd55ed8bd6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
- AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7));
+ AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15);
AddStep("clear results", () => Player.Results.Clear());
addSeekStep(0);
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index da6604a653..a984f508ea 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Tests.Gameplay;
@@ -148,6 +149,42 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
}
+ [Test]
+ public void TestInputDoesntWorkWhenHUDHidden()
+ {
+ SongProgressBar getSongProgress() => hudOverlay.ChildrenOfType().Single();
+
+ bool seeked = false;
+
+ createNew();
+
+ AddStep("bind seek", () =>
+ {
+ seeked = false;
+
+ var progress = getSongProgress();
+
+ progress.ShowHandle = true;
+ progress.OnSeek += _ => seeked = true;
+ });
+
+ AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
+ AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
+
+ AddStep("attempt seek", () =>
+ {
+ InputManager.MoveMouseTo(getSongProgress());
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("seek not performed", () => !seeked);
+
+ AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
+
+ AddStep("attempt seek", () => InputManager.Click(MouseButton.Left));
+ AddAssert("seek performed", () => seeked);
+ }
+
[Test]
public void TestHiddenHUDDoesntBlockComponentUpdates()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 8a4818d2f8..156a1ee34a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -11,8 +11,10 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
+using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@@ -167,14 +169,39 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
- private void addHitObject(double time)
+ [Test]
+ public void TestVeryFlowScroll()
+ {
+ const double long_time_range = 100000;
+ var manualClock = new ManualClock();
+
+ AddStep("set manual clock", () =>
+ {
+ manualClock.CurrentTime = 0;
+ scrollContainers.ForEach(c => c.Clock = new FramedClock(manualClock));
+
+ setScrollAlgorithm(ScrollVisualisationMethod.Constant);
+ scrollContainers.ForEach(c => c.TimeRange = long_time_range);
+ });
+
+ AddStep("add hit objects", () =>
+ {
+ addHitObject(long_time_range);
+ addHitObject(long_time_range + 100, 250);
+ });
+
+ AddAssert("hit objects are alive", () => playfields.All(p => p.HitObjectContainer.AliveObjects.Count() == 2));
+ }
+
+ private void addHitObject(double time, float size = 75)
{
playfields.ForEach(p =>
{
- var hitObject = new TestDrawableHitObject(time);
- setAnchor(hitObject, p);
+ var hitObject = new TestHitObject(size) { StartTime = time };
+ var drawable = new TestDrawableHitObject(hitObject);
- p.Add(hitObject);
+ setAnchor(drawable, p);
+ p.Add(drawable);
});
}
@@ -248,6 +275,8 @@ namespace osu.Game.Tests.Visual.Gameplay
}
};
}
+
+ protected override ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new TestScrollingHitObjectContainer();
}
private class TestDrawableControlPoint : DrawableHitObject
@@ -281,22 +310,41 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
- private class TestDrawableHitObject : DrawableHitObject
+ private class TestHitObject : HitObject
{
- public TestDrawableHitObject(double time)
- : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
- {
- Origin = Anchor.Custom;
- OriginPosition = new Vector2(75 / 4.0f);
+ public readonly float Size;
- AutoSizeAxes = Axes.Both;
+ public TestHitObject(float size)
+ {
+ Size = size;
+ }
+ }
+
+ private class TestDrawableHitObject : DrawableHitObject
+ {
+ public TestDrawableHitObject(TestHitObject hitObject)
+ : base(hitObject)
+ {
+ Origin = Anchor.Centre;
+ Size = new Vector2(hitObject.Size);
AddInternal(new Box
{
- Size = new Vector2(75),
+ RelativeSizeAxes = Axes.Both,
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)
});
}
}
+
+ private class TestScrollingHitObjectContainer : ScrollingHitObjectContainer
+ {
+ protected override RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry)
+ {
+ if (entry.HitObject is TestHitObject testObject)
+ return new RectangleF().Inflate(testObject.Size / 2);
+
+ return base.GetConservativeBoundingBox(entry);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
index c852685b74..60ed0012ae 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
@@ -66,7 +66,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v);
AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v);
- AddSliderStep("combo", 0, 1000, 0, v => scoreProcessor.HighestCombo.Value = v);
+ AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v);
+ AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
index 5e76fe1519..003cec0d07 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
@@ -9,8 +9,10 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
@@ -92,6 +94,31 @@ namespace osu.Game.Tests.Visual.Navigation
returnToMenu();
}
+ [Test]
+ public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
+ {
+ AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke());
+ AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
+
+ AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq");
+ AddUntilStep("wait for no results", () => Beatmap.IsDefault);
+
+ var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
+ presentAndConfirm(firstImport, type);
+ }
+
+ [Test]
+ public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type)
+ {
+ AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke());
+ AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
+
+ AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
+
+ var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
+ presentAndConfirm(firstImport, type);
+ }
+
[Test]
public void TestFromSongSelect([Values] ScorePresentType type)
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
index 39432ee059..863b352618 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs
@@ -189,6 +189,16 @@ Line after image";
});
}
+ [Test]
+ public void TestFlag()
+ {
+ AddStep("Add flag", () =>
+ {
+ markdownContainer.CurrentPath = @"https://dev.ppy.sh";
+ markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
+ });
+ }
+
private class TestMarkdownContainer : WikiMarkdownContainer
{
public LinkInline Link;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
index 558bff2f3c..27cd74bb1f 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
@@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.Online
Locale = "en",
Subtitle = "Article styling criteria",
Markdown =
- "# Formatting\n\n*For the writing standards, see: [Article style criteria/Writing](../Writing)*\n\n*Notice: This article uses [RFC 2119](https://tools.ietf.org/html/rfc2119 \"IETF Tools\") to describe requirement levels.*\n\n## Locales\n\nListed below are the properly-supported locales for the wiki:\n\n| File Name | Locale Name | Native Script |\n| :-- | :-- | :-- |\n| `en.md` | English | English |\n| `ar.md` | Arabic | اَلْعَرَبِيَّةُ |\n| `be.md` | Belarusian | Беларуская мова |\n| `bg.md` | Bulgarian | Български |\n| `cs.md` | Czech | Česky |\n| `da.md` | Danish | Dansk |\n| `de.md` | German | Deutsch |\n| `gr.md` | Greek | Ελληνικά |\n| `es.md` | Spanish | Español |\n| `fi.md` | Finnish | Suomi |\n| `fr.md` | French | Français |\n| `hu.md` | Hungarian | Magyar |\n| `id.md` | Indonesian | Bahasa Indonesia |\n| `it.md` | Italian | Italiano |\n| `ja.md` | Japanese | 日本語 |\n| `ko.md` | Korean | 한국어 |\n| `nl.md` | Dutch | Nederlands |\n| `no.md` | Norwegian | Norsk |\n| `pl.md` | Polish | Polski |\n| `pt.md` | Portuguese | Português |\n| `pt-br.md` | Brazilian Portuguese | Português (Brasil) |\n| `ro.md` | Romanian | Română |\n| `ru.md` | Russian | Русский |\n| `sk.md` | Slovak | Slovenčina |\n| `sv.md` | Swedish | Svenska |\n| `th.md` | Thai | ไทย |\n| `tr.md` | Turkish | Türkçe |\n| `uk.md` | Ukrainian | Українська мова |\n| `vi.md` | Vietnamese | Tiếng Việt |\n| `zh.md` | Chinese (Simplified) | 简体中文 |\n| `zh-tw.md` | Traditional Chinese (Taiwan) | 繁體中文(台灣) |\n\n*Note: The website will give readers their selected language's version of an article. If it is not available, the English version will be given.*\n\n### Content parity\n\nTranslations are subject to strict content parity with their English article, in the sense that they must have the same message, regardless of grammar and syntax. Any changes to the translations' meanings must be accompanied by equivalent changes to the English article.\n\nThere are some cases where the content is allowed to differ:\n\n- Articles originally written in a language other than English (in this case, English should act as the translation)\n- Explanations of English words that are common terms in the osu! community\n- External links\n- Tags\n- Subcommunity-specific explanations\n\n## Front matter\n\nFront matter must be placed at the very top of the file. It is written in [YAML](https://en.wikipedia.org/wiki/YAML#Example \"YAML Wikipedia article\") and describes additional information about the article. This must be surrounded by three hyphens (`---`) on the lines above and below it, and an empty line must follow it before the title heading.\n\n### Articles that need help\n\n*Note: Avoid translating English articles with this tag. In addition to this, this tag should be added when the translation needs its own clean up.*\n\nThe `needs_cleanup` tag may be added to articles that need rewriting or formatting help. It is also acceptable to open an issue on GitHub for this purpose. This tag must be written as shown below:\n\n```yaml\nneeds_cleanup: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be done to remove the tag.\n\n### Outdated articles\n\n*Note: Avoid translating English articles with this tag. If the English article has this tag, the translation must also have this tag.*\n\nTranslated articles that are outdated must use the `outdated` tag when the English variant is updated. English articles may also become outdated when the content they contain is misleading or no longer relevant. This tag must be written as shown below:\n\n```yaml\noutdated: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be updated to remove the tag.\n\n### Tagging articles\n\nTags help the website's search engine query articles better. Tags should be written in the same language as the article and include the original list of tags. Tags should use lowercase letters where applicable.\n\nFor example, an article called \"Beatmap discussion\" may include the following tags:\n\n```yaml\ntags:\n - beatmap discussions\n - modding V2\n - MV2\n```\n\n### Translations without reviews\n\n*Note: Wiki maintainers will determine and apply this mark prior to merging.*\n\nSometimes, translations are added to the wiki without review from other native speakers of the language. In this case, the `no_native_review` mark is added to let future translators know that it may need to be checked again. This tag must be written as shown below:\n\n```yaml\nno_native_review: true\n```\n\n## Article naming\n\n*See also: [Folder names](#folder-names) and [Titles](#titles)*\n\nArticle titles should be singular and use sentence case. See [Wikipedia's naming conventions article](https://en.wikipedia.org/wiki/Wikipedia:Naming_conventions_(plurals) \"Wikipedia\") for more details.\n\nArticle titles should match the folder name it is in (spaces may replace underscores (`_`) where appropriate). If the folder name changes, the article title should be changed to match it and vice versa.\n\n---\n\nContest and tournament articles are an exception. The folder name must use abbreviations, acronyms, or initialisms. The article's title must be the full name of the contest or tournament.\n\n## Folder and file structure\n\n### Folder names\n\n*See also: [Article naming](#article-naming)*\n\nFolder names must be in English and use sentence case.\n\nFolder names must only use these characters:\n\n- uppercase and lowercase letters\n- numbers\n- underscores (`_`)\n- hyphens (`-`)\n- exclamation marks (`!`)\n\n### Article file names\n\nThe file name of an article can be found in the `File Name` column of the [locales section](#locales). The location of a translated article must be placed in the same folder as the English article.\n\n### Index articles\n\nAn index article must be created if the folder is intended to only hold other articles. Index articles must contain a list of articles that are inside its own folder. They may also contain other information, such as a lead paragraph or descriptions of the linked articles.\n\n### Disambiguation articles\n\n[Disambiguation](/wiki/Disambiguation) articles must be placed in the `/wiki/Disambiguation` folder. The main page must be updated to include the disambiguation article. Refer to [Disambiguation/Mod](/wiki/Disambiguation/Mod) as an example.\n\nRedirects must be updated to have the ambiguous keyword(s) redirect to the disambiguation article.\n\nArticles linked from a disambiguation article must have a [For other uses](#for-other-uses) hatnote.\n\n## HTML\n\nHTML must not be used, with exception for [comments](#comments). The structure of the article must be redone if HTML is used.\n\n### Comments\n\nHTML comments should be used for marking to-dos, but may also be used to annotate text. They should be on their own line, but can be placed inline in a paragraph. If placed inline, the start of the comment must not have a space.\n\nBad example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\nGood example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\n## Editing\n\n### End of line sequence\n\n*Caution: Uploading Markdown files using `CRLF` (carriage return and line feed) via GitHub will result in those files using `CRLF`. To prevent this, set the line ending to `LF` (line feed) before uploading.*\n\nMarkdown files must be checked in using the `LF` end of line sequence.\n\n### Escaping\n\nMarkdown syntax should be escaped as needed. However, article titles are parsed as plain text and so must not be escaped.\n\n### Paragraphs\n\nEach paragraph must be followed by one empty line.\n\n### Line breaks\n\nLine breaks must use a backslash (`\\`).\n\nLine breaks must be used sparingly.\n\n## Hatnote\n\n*Not to be confused with [Notice](#notice).*\n\nHatnotes are short notes placed at the top of an article or section to help readers navigate to related articles or inform them about related topics.\n\nHatnotes must be italicised and be placed immediately after the heading. If multiple hatnotes are used, they must be on the same paragraph separated with a line break.\n\n### Main page\n\n*Main page* hatnotes direct the reader to the main article of a topic. When this hatnote is used, it implies that the section it is on is a summary of what the linked page is about. This hatnote should have only one link. These must be formatted as follows:\n\n```markdown\n*Main page: {article}*\n\n*Main pages: {article} and {article}*\n```\n\n### See also\n\n*See also* hatnotes suggest to readers other points of interest from a given article or section. These must be formatted as follows:\n\n```markdown\n*See also: {article}*\n\n*See also: {article} and {article}*\n```\n\n### For see\n\n*For see* hatnotes are similar to *see also* hatnotes, but are generally more descriptive and direct. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*For {description}, see: {article}`*\n\n*For {description}, see: {article} and {article}`*\n```\n\n### Not to be confused with\n\n*Not to be confused with* hatnotes help distinguish ambiguous or misunderstood article titles or sections. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*Not to be confused with {article}.*\n\n*Not to be confused with {article} or {article}.*\n```\n\n### For other uses\n\n*For other uses* hatnotes are similar to *not to be confused with* hatnotes, but links directly to the [disambiguation article](#disambiguation-articles). This hatnote must only link to the disambiguation article. These must be formatted as follows:\n\n```markdown\n*For other uses, see {disambiguation article}.*\n```\n\n## Notice\n\n*Not to be confused with [Hatnote](#hatnote).*\n\nA notice should be placed where appropriate in a section, but must start off the paragraph and use italics. Notices may contain bolding where appropriate, but should be kept to a minimum. Notices must be written as complete sentences. Thus, unlike most [hatnotes](#hatnotes), must use a full stop (`.`) or an exclamation mark (`!`) if appropriate. Anything within the same paragraph of a notice must also be italicised. These must be formatted as follows:\n\n```markdown\n*Note: {note}.*\n\n*Notice: {notice}.*\n\n*Caution: {caution}.*\n\n*Warning: {warning}.*\n```\n\n- `Note` should be used for factual or trivial details.\n- `Notice` should be used for reminders or to draw attention to something that the reader should be made aware of.\n- `Caution` should be used to warn the reader to avoid unintended consequences.\n- `Warning` should be used to warn the reader that action may be taken against them.\n\n## Emphasising\n\n### Bold\n\nBold must use double asterisks (`**`).\n\nLead paragraphs may bold the first occurrence of the article's title.\n\n### Italics\n\nItalics must use single asterisks (`*`).\n\nNames of work or video games should be italicised. osu!—the game—is exempt from this.\n\nThe first occurrence of an abbreviation, acronym, or initialism may be italicised.\n\nItalics may also be used to provide emphasis or help with readability.\n\n## Headings\n\nAll headings must use sentence case.\n\nHeadings must use the [ATX (hash) style](https://github.github.com/gfm/#atx-headings \"GitHub\") and must have an empty line before and after the heading. The title heading is an exception when it is on the first line. If this is the case, there only needs to be an empty line after the title heading.\n\nHeadings must not exceed a heading level of 5 and must not be used to style or format text.\n\n### Titles\n\n*See also: [Article naming](#article-naming)*\n\n*Caution: Titles are parsed as plain text; they must not be escaped.*\n\nThe first heading in all articles must be a level 1 heading, being the article's title. All headings afterwards must be [section headings](#sections). Titles must not contain formatting, links, or images.\n\nThe title heading must be on the first line, unless [front matter](#front-matter) is being used. If that is the case, the title heading must go after it and have an empty line before the title heading.\n\n### Sections\n\nSection headings must use levels 2 to 5. The section heading proceeding the [title heading](#titles) must be a level 2 heading. Unlike titles, section headings may have small image icons.\n\nSection headings must not skip a heading level (i.e. do not go from a level 2 heading to a level 4 heading) and must not contain formatting or links.\n\n*Notice: On the website, heading levels 4 and 5 will not appear in the table of contents. They cannot be linked to directly either.*\n\n## Lists\n\nLists should not go over 4 levels of indentation and should not have an empty line in between each item.\n\nFor nested lists, bullets or numbers must align with the item content of their parent lists.\n\nThe following example was done incorrectly (take note of the spacing before the bullet):\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\nThe following example was done correctly:\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\n### Bulleted\n\nBulleted lists must use a hyphen (`-`). These must then be followed by one space. (Example shown below.)\n\n```markdown\n- osu!\n - Hit circle\n - Combo number\n - Approach circle\n - Slider\n - Hit circles\n - Slider body\n - Slider ticks\n - Spinner\n- osu!taiko\n```\n\n### Numbered\n\nThe numbers in a numbered list must be incremented to represent their step.\n\n```markdown\n1. Download the osu! installer.\n2. Run the installer.\n 1. To change the installation location, click the text underneath the progression bar.\n 2. The installer will prompt for a new location, choose the installation folder.\n3. osu! will start up once installation is complete.\n4. Sign in.\n```\n\n### Mixed\n\nCombining both bulleted and numbered lists should be done sparingly.\n\n```markdown\n1. Download a skin from the forums.\n2. Load the skin file into osu!.\n - If the file is a `.zip`, unzip it and move the contents into the `Skins/` folder (found in your osu! installation folder).\n - If the file is a `.osk`, open it on your desktop or drag-and-drop it into the game client.\n3. Open osu!, if it is not opened, and select the skin in the options.\n - This may have been completed if you opened the `.osk` file or drag-and-dropped it into the game client.\n```\n\n## Code\n\nThe markup for code is a grave mark (`` ` ``). To put grave marks in code, use double grave marks instead. If the grave mark is at the start or end, pad it with one space. (Example shown below.)\n\n```markdown\n`` ` ``\n`` `Space` ``\n```\n\n### Keyboard keys\n\n*Notice: When denoting the letter itself, and not the keyboard key, use quotation marks instead.*\n\nWhen representing keyboard keys, use capital letters for single characters and title case for modifiers. Use the plus symbol (`+`) (without code) to represent key combinations. (Example shown below.)\n\n```markdown\npippi is spelt with a lowercase \"p\" like peppy.\n\nPress `Ctrl` + `O` to open the open dialog.\n```\n\nWhen representing a space or the spacebar, use `` `Space` ``.\n\n### Button and menu text\n\nWhen copying the text from a menu or button, the letter casing should be copied as it appears. (Example shown below.)\n\n```markdown\nThe `osu!direct` button is visible in the main menu on the right side, if you have an active osu!supporter tag.\n```\n\n### Folder and directory names\n\nWhen copying the name of a folder or directory, the letter casing should be copied as it appears, but prefer lowercased paths when possible. Directory paths must not be absolute (i.e. do not start the directory name from the drive letter or from the root folder). (Example shown below.)\n\n```markdown\nosu! is installed in the `AppData/Local` folder by default, unless specified otherwise during installation.\n```\n\n### Keywords and commands\n\nWhen copying a keyword or command, the letter casing should be copied as it appears or how someone normally would type it. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nAs of now, the `Name` and `Author` commands in the skin configuration file (`skin.ini`) do nothing.\n```\n\n### File names\n\nWhen copying the name of a file, the letter casing should be copied as it appears. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nTo play osu!, double click the `osu!.exe` icon.\n```\n\n### File extensions\n\n*Notice: File formats (not to be confused with file extensions) must be written in capital letters without the prefixed fullstop (`.`).*\n\nFile extensions must be prefixed with a fullstop (`.`) and be followed by the file extension in lowercase letters. (Example shown below.)\n\n```markdown\nThe JPG (or JPEG) file format has the `.jpg` (or `.jpeg`) extension.\n```\n\n### Chat channels\n\nWhen copying the name of a chat channel, start it with a hash (`#`), followed by the channel name in lowercase letters. (Example shown below.)\n\n```markdown\n`#lobby` is where you can advertise your multi room.\n```\n\n## Preformatted text (code blocks)\n\n*Notice: Syntax highlighting for preformatted text is not implemented on the website yet.*\n\nPreformatted text (also known as code blocks) must be fenced using three grave marks. They should set the language identifier for syntax highlighting.\n\n## Links\n\nThere are two types of links: inline and reference. Inline has two styles.\n\nThe following is an example of both inline styles:\n\n```markdown\n[Game Modifiers](/wiki/Game_Modifiers)\n\n\n```\n\nThe following is an example of the reference style:\n\n```markdown\n[Game Modifiers][game mods link]\n\n[game mods link]: /wiki/Game_Modifiers\n```\n\n---\n\nLinks must use the inline style if they are only referenced once. The inline angle brackets style should be avoided. References to reference links must be placed at the bottom of the article.\n\n### Internal links\n\n*Note: Internal links refer to links that stay inside the `https://osu.ppy.sh/` domain.*\n\n#### Wiki links\n\nAll links that point to an wiki article should start with `/wiki/` followed by the path to get to the article you are targeting. Relative links may also be used. Some examples include the following:\n\n```markdown\n[FAQ](/wiki/FAQ)\n[pippi](/wiki/Mascots#-pippi)\n[Beatmaps](../)\n[Pattern](./Pattern)\n```\n\nWiki links must not use redirects and must not have a trailing forward slash (`/`).\n\nBad examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/ASC)\n[Developers](/wiki/Developers/)\n[Developers](/wiki/Developers/#game-client-developers)\n```\n\nGood examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/Article_styling_criteria)\n[Developers](/wiki/Developers)\n[Developers](/wiki/Developers#game-client-developers)\n```\n\n##### Sub-article links\n\nWiki links that point to a sub-article should include the parent article's folder name in its link text. See the following example:\n\n```markdown\n*See also: [Beatmap Editor/Design](/wiki/Beatmap_Editor/Design)*\n```\n\n##### Section links\n\n*Notice: On the website, heading levels 4 and 5 are not given the id attribute. This means that they can not be linked to directly.*\n\nWiki links that point to a section of an article may use the section sign symbol (`§`). See the following example:\n\n```markdown\n*For timing rules, see: [Ranking Criteria § Timing](/wiki/Ranking_Criteria#timing)*\n```\n\n#### Other osu! links\n\nThe URL from the address bar of your web browser should be copied as it is when linking to other osu! web pages. The `https://osu.ppy.sh` part of the URL must be kept.\n\n##### User profiles\n\nAll usernames must be linked on first occurrence. Other occurrences are optional, but must be consistent throughout the entire article for all usernames. If it is difficult to determine the user's id, it may be skipped over.\n\nWhen linking to a user profile, the user's id number must be used. Use the new website (`https://osu.ppy.sh/users/{username})`) to get the user's id.\n\nThe link text of the user link should be the user's current name.\n\n##### Difficulties\n\nWhenever linking to a single difficulty, use this format as the link text:\n\n```\n{artist} - {title} ({creator}) [{difficuty_name}]\n```\n\nThe link must actually link to that difficulty. Beatmap difficulty URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}#{mode}/{BeatmapID}\n```\n\nThe difficulty name may be left outside of the link text, but doing so must be consistent throughout the entire article.\n\n##### Beatmaps\n\nWhenever linking to a beatmap, use this format as the link text:\n\n```\n{artist} - {title} ({creator})\n```\n\nAll beatmap URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}\n```\n\n### External links\n\n*Notice: External links refers to links that go outside the `https://osu.ppy.sh/` domain.*\n\nThe `https` protocol must be used, unless the site does not support it. External links must be a clean and direct link to a reputable source. The link text should be the title of the page it is linking to. The URL from the address bar of your web browser should be copied as it is when linking to other external pages.\n\nThere are no visual differences between external and osu! web links. Due to this, the website name should be included in the title text. See the following example:\n\n```markdown\n*For more information about music theory, see: [Music theory](https://en.wikipedia.org/wiki/Music_theory \"Wikipedia\")*\n```\n\n## Images\n\nThere are two types of image links: inline and reference. Examples:\n\n**Inline style:**\n\n```markdown\n\n```\n\n**Reference style:**\n\n```markdown\n![][flag_AU]\n\n[flag_AU]: /wiki/shared/flag/AU.gif\n```\n\nImages should use the inline linking style. References to reference links must be placed at the bottom of the article.\n\nImages must be placed in a folder named `img`, located in the article's folder. Images that are used in multiple articles should be stored in the `/wiki/shared/` folder.\n\n### Image caching\n\nImages on the website are cached for up to 60 days. The cached image is matched with the image link's URL.\n\nWhen updating an image, either change the image's name or append a query string to the URL. In both cases, all translations linking to the updated image should also be updated.\n\n### Formats and quality\n\nImages should use the JPG format at quality 8 (80 or 80%, depending on the program). If the image contains transparency or has text that must be readable, use the PNG format instead. If the image contains an animation, the GIF format can be used; however, this should be used sparingly as these may take longer to load or can be bigger then the [max file size](#file-size).\n\n### File size\n\nImages must be under 1 megabyte, otherwise they will fail to load. Downscaling and using JPG at 80% is almost always under the size limit.\n\nAll images should be optimised as much as possible. Use [jpeg-archive](https://github.com/danielgtaylor/jpeg-archive \"GitHub\") to compress JPEG images. For consistency, use the following command for jpeg-archive:\n\n```sh\njpeg-recompress -am smallfry
public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections");
+ ///
+ /// "Mod presets"
+ ///
+ public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"Mod presets");
+
///
/// "Name"
///
diff --git a/osu.Game/Localisation/DebugSettingsStrings.cs b/osu.Game/Localisation/DebugSettingsStrings.cs
index 74b2c8d892..dd21739096 100644
--- a/osu.Game/Localisation/DebugSettingsStrings.cs
+++ b/osu.Game/Localisation/DebugSettingsStrings.cs
@@ -44,11 +44,6 @@ namespace osu.Game.Localisation
///
public static LocalisableString ClearAllCaches => new TranslatableString(getKey(@"clear_all_caches"), @"Clear all caches");
- ///
- /// "Compact realm"
- ///
- public static LocalisableString CompactRealm => new TranslatableString(getKey(@"compact_realm"), @"Compact realm");
-
private static string getKey(string key) => $"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/GeneralSettingsStrings.cs b/osu.Game/Localisation/GeneralSettingsStrings.cs
index 2aa91f5245..3278b20983 100644
--- a/osu.Game/Localisation/GeneralSettingsStrings.cs
+++ b/osu.Game/Localisation/GeneralSettingsStrings.cs
@@ -64,6 +64,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard");
+ ///
+ /// "You are running the latest release ({0})"
+ ///
+ public static LocalisableString RunningLatestRelease(string version) => new TranslatableString(getKey(@"running_latest_release"), @"You are running the latest release ({0})", version);
+
private static string getKey(string key) => $"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/MaintenanceSettingsStrings.cs b/osu.Game/Localisation/MaintenanceSettingsStrings.cs
index a398eced07..8aa0adf7a0 100644
--- a/osu.Game/Localisation/MaintenanceSettingsStrings.cs
+++ b/osu.Game/Localisation/MaintenanceSettingsStrings.cs
@@ -19,6 +19,41 @@ namespace osu.Game.Localisation
///
public static LocalisableString SelectDirectory => new TranslatableString(getKey(@"select_directory"), @"Select directory");
+ ///
+ /// "Migration in progress"
+ ///
+ public static LocalisableString MigrationInProgress => new TranslatableString(getKey(@"migration_in_progress"), @"Migration in progress");
+
+ ///
+ /// "This could take a few minutes depending on the speed of your disk(s)."
+ ///
+ public static LocalisableString MigrationDescription => new TranslatableString(getKey(@"migration_description"), @"This could take a few minutes depending on the speed of your disk(s).");
+
+ ///
+ /// "Please avoid interacting with the game!"
+ ///
+ public static LocalisableString ProhibitedInteractDuringMigration => new TranslatableString(getKey(@"prohibited_interact_during_migration"), @"Please avoid interacting with the game!");
+
+ ///
+ /// "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up."
+ ///
+ public static LocalisableString FailedCleanupNotification => new TranslatableString(getKey(@"failed_cleanup_notification"), @"Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.");
+
+ ///
+ /// "Please select a new location"
+ ///
+ public static LocalisableString SelectNewLocation => new TranslatableString(getKey(@"select_new_location"), @"Please select a new location");
+
+ ///
+ /// "The target directory already seems to have an osu! install. Use that data instead?"
+ ///
+ public static LocalisableString TargetDirectoryAlreadyInstalledOsu => new TranslatableString(getKey(@"target_directory_already_installed_osu"), @"The target directory already seems to have an osu! install. Use that data instead?");
+
+ ///
+ /// "To complete this operation, osu! will close. Please open it again to use the new data location."
+ ///
+ public static LocalisableString RestartAndReOpenRequiredForCompletion => new TranslatableString(getKey(@"restart_and_re_open_required_for_completion"), @"To complete this operation, osu! will close. Please open it again to use the new data location.");
+
///
/// "Import beatmaps from stable"
///
@@ -84,6 +119,26 @@ namespace osu.Game.Localisation
///
public static LocalisableString RestoreAllRecentlyDeletedModPresets => new TranslatableString(getKey(@"restore_all_recently_deleted_mod_presets"), @"Restore all recently deleted mod presets");
+ ///
+ /// "Deleted all collections!"
+ ///
+ public static LocalisableString DeletedAllCollections => new TranslatableString(getKey(@"deleted_all_collections"), @"Deleted all collections!");
+
+ ///
+ /// "Deleted all mod presets!"
+ ///
+ public static LocalisableString DeletedAllModPresets => new TranslatableString(getKey(@"deleted_all_mod_presets"), @"Deleted all mod presets!");
+
+ ///
+ /// "Restored all deleted mod presets!"
+ ///
+ public static LocalisableString RestoredAllDeletedModPresets => new TranslatableString(getKey(@"restored_all_deleted_mod_presets"), @"Restored all deleted mod presets!");
+
+ ///
+ /// "Please select your osu!stable install location"
+ ///
+ public static LocalisableString StableDirectorySelectHeader => new TranslatableString(getKey(@"stable_directory_select_header"), @"Please select your osu!stable install location");
+
private static string getKey(string key) => $"{prefix}:{key}";
}
}
diff --git a/osu.Game/Localisation/TabletSettingsStrings.cs b/osu.Game/Localisation/TabletSettingsStrings.cs
index d62d348df9..6c2e3c1f9c 100644
--- a/osu.Game/Localisation/TabletSettingsStrings.cs
+++ b/osu.Game/Localisation/TabletSettingsStrings.cs
@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString NoTabletDetected => new TranslatableString(getKey(@"no_tablet_detected"), @"No tablet detected!");
+ ///
+ /// "If your tablet is not detected, please read [this FAQ]({0}) for troubleshooting steps."
+ ///
+ public static LocalisableString NoTabletDetectedDescription(string url) => new TranslatableString(getKey(@"no_tablet_detected_description"), @"If your tablet is not detected, please read [this FAQ]({0}) for troubleshooting steps.", url);
+
///
/// "Reset to full area"
///
diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
index a012bf49b6..48d5c0bea9 100644
--- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
+++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Diagnostics;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -58,7 +58,7 @@ namespace osu.Game.Online.Spectator
{
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
}
- catch (HubException exception)
+ catch (Exception exception)
{
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
{
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 59c0af1533..1716e48395 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -561,9 +561,20 @@ namespace osu.Game
return;
}
+ // This should be able to be performed from song select, but that is disabled for now
+ // due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios).
+ //
+ // As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select.
+ // This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the
+ // song select leaderboard).
+ IEnumerable validScreens =
+ Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)
+ ? new[] { typeof(SongSelect) }
+ : Array.Empty();
+
PerformFromScreen(screen =>
{
- Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset} to match score");
+ Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score");
Ruleset.Value = databasedScore.ScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
@@ -578,7 +589,7 @@ namespace osu.Game
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
break;
}
- }, validScreens: new[] { typeof(PlaySongSelect) });
+ }, validScreens: validScreens);
}
public override Task Import(params ImportTask[] imports)
diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs
index 8be865ee16..c17080f602 100644
--- a/osu.Game/Overlays/Dialog/ConfirmDialog.cs
+++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs
@@ -5,6 +5,7 @@
using System;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Dialog
@@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Dialog
/// The description of the action to be displayed to the user.
/// An action to perform on confirmation.
/// An optional action to perform on cancel.
- public ConfirmDialog(string message, Action onConfirm, Action onCancel = null)
+ public ConfirmDialog(LocalisableString message, Action onConfirm, Action onCancel = null)
{
HeaderText = message;
BodyText = "Last chance to turn back";
diff --git a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
index 42ac4adb34..3afb060e49 100644
--- a/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/DebugSettings/MemorySettings.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
},
new SettingsButton
{
- Text = DebugSettingsStrings.CompactRealm,
+ Text = "Compact realm",
Action = () =>
{
// Blocking operations implicitly causes a Compact().
diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
index 63f0dec953..0f77e6609b 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
@@ -44,9 +44,12 @@ namespace osu.Game.Overlays.Settings.Sections.General
},
};
- if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale))
- locale = Language.en;
- languageSelection.Current.Value = locale;
+ frameworkLocale.BindValueChanged(locale =>
+ {
+ if (!LanguageExtensions.TryParseCultureCode(locale.NewValue, out var language))
+ language = Language.en;
+ languageSelection.Current.Value = language;
+ }, true);
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
}
diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
index b68a4fed48..d97cf699e5 100644
--- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
notifications?.Post(new SimpleNotification
{
- Text = $"You are running the latest release ({game.Version})",
+ Text = GeneralSettingsStrings.RunningLatestRelease(game.Version),
Icon = FontAwesome.Solid.CheckCircle,
});
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 42edd49a47..59b56522a4 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -77,8 +77,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
window.DisplaysChanged += onDisplaysChanged;
}
- if (host.Window is WindowsWindow windowsWindow)
- fullscreenCapability.BindTo(windowsWindow.FullscreenCapability);
+ if (host.Renderer is IWindowsRenderer windowsRenderer)
+ fullscreenCapability.BindTo(windowsRenderer.FullscreenCapability);
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
index 9ff47578e9..2fea2e34b2 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
@@ -387,14 +387,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (bindTarget != null) bindTarget.IsBinding = true;
}
- private void updateStoreFromButton(KeyButton button)
- {
- realm.Run(r =>
- {
- var binding = r.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID);
- r.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString);
- });
- }
+ private void updateStoreFromButton(KeyButton button) =>
+ realm.WriteAsync(r => r.Find(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString);
private void updateIsDefaultValue()
{
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 544259ddf1..f1e216f538 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OsuColour colours, LocalisationManager localisation)
{
Children = new Drawable[]
{
@@ -110,11 +110,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
{
t.NewLine();
- t.AddText("If your tablet is not detected, please read ");
- t.AddLink("this FAQ", LinkAction.External, RuntimeInfo.OS == RuntimeInfo.Platform.Windows
+ var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription(RuntimeInfo.OS == RuntimeInfo.Platform.Windows
? @"https://opentabletdriver.net/Wiki/FAQ/Windows"
- : @"https://opentabletdriver.net/Wiki/FAQ/Linux");
- t.AddText(" for troubleshooting steps.");
+ : @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value);
+ t.AddLinks(formattedSource.Text, formattedSource.Links);
}
}),
}
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs
index 453dbd2e18..beae5a6aad 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/BeatmapSettings.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class BeatmapSettings : SettingsSubsection
{
- protected override LocalisableString Header => "Beatmaps";
+ protected override LocalisableString Header => CommonStrings.Beatmaps;
private SettingsButton importBeatmapsButton = null!;
private SettingsButton deleteBeatmapsButton = null!;
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs
index 5a91213eb8..17fef37e40 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/CollectionsSettings.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class CollectionsSettings : SettingsSubsection
{
- protected override LocalisableString Header => "Collections";
+ protected override LocalisableString Header => CommonStrings.Collections;
private SettingsButton importCollectionsButton = null!;
@@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private void deleteAllCollections()
{
realm.Write(r => r.RemoveAll());
- notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" });
+ notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllCollections });
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs
index d565576d09..158e1a8aa0 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationRunScreen.cs
@@ -15,6 +15,7 @@ using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Localisation;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens;
using osuTK;
@@ -71,14 +72,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Text = "Migration in progress",
+ Text = MaintenanceSettingsStrings.MigrationInProgress,
Font = OsuFont.Default.With(size: 40)
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Text = "This could take a few minutes depending on the speed of your disk(s).",
+ Text = MaintenanceSettingsStrings.MigrationDescription,
Font = OsuFont.Default.With(size: 30)
},
new LoadingSpinner(true)
@@ -89,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Text = "Please avoid interacting with the game!",
+ Text = MaintenanceSettingsStrings.ProhibitedInteractDuringMigration,
Font = OsuFont.Default.With(size: 30)
},
}
@@ -111,7 +112,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
notifications.Post(new SimpleNotification
{
- Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.",
+ Text = MaintenanceSettingsStrings.FailedCleanupNotification,
Activated = () =>
{
originalStorage.PresentExternally();
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs
index 0d32e33d87..5de33fdd55 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs
@@ -12,6 +12,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.IO;
+using osu.Game.Localisation;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
@@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
public override bool HideOverlaysOnEnter => true;
- public override LocalisableString HeaderText => "Please select a new location";
+ public override LocalisableString HeaderText => MaintenanceSettingsStrings.SelectNewLocation;
protected override void OnSelection(DirectoryInfo directory)
{
@@ -51,9 +52,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
// Quick test for whether there's already an osu! install at the target path.
if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME))
{
- dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use that data instead?", () =>
+ dialogOverlay.Push(new ConfirmDialog(MaintenanceSettingsStrings.TargetDirectoryAlreadyInstalledOsu, () =>
{
- dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () =>
+ dialogOverlay.Push(new ConfirmDialog(MaintenanceSettingsStrings.RestartAndReOpenRequiredForCompletion, () =>
{
(storage as OsuStorage)?.ChangeDataPath(target.FullName);
game.Exit();
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs
index d35d3ff468..51f6e1bf60 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/ModPresetSettings.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class ModPresetSettings : SettingsSubsection
{
- protected override LocalisableString Header => "Mod presets";
+ protected override LocalisableString Header => CommonStrings.ModPresets;
[Resolved]
private RealmAccess realm { get; set; } = null!;
@@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
deleteAllButton.Enabled.Value = true;
if (deletionTask.IsCompletedSuccessfully)
- notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all mod presets!" });
+ notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllModPresets });
else if (deletionTask.IsFaulted)
Logger.Error(deletionTask.Exception, "Failed to delete all mod presets");
}
@@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
undeleteButton.Enabled.Value = true;
if (undeletionTask.IsCompletedSuccessfully)
- notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Restored all deleted mod presets!" });
+ notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.RestoredAllDeletedModPresets });
else if (undeletionTask.IsFaulted)
Logger.Error(undeletionTask.Exception, "Failed to restore mod presets");
}
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs
index 70e11d45dc..eb2d3171ea 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/ScoreSettings.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class ScoreSettings : SettingsSubsection
{
- protected override LocalisableString Header => "Scores";
+ protected override LocalisableString Header => CommonStrings.Scores;
private SettingsButton importScoresButton = null!;
private SettingsButton deleteScoresButton = null!;
diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs
index c95b1d4dd8..93c65513b7 100644
--- a/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Maintenance/SkinSettings.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class SkinSettings : SettingsSubsection
{
- protected override LocalisableString Header => "Skins";
+ protected override LocalisableString Header => CommonStrings.Skins;
private SettingsButton importSkinsButton = null!;
private SettingsButton deleteSkinsButton = null!;
diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
index 4f1083a75c..15c455416c 100644
--- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
+++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Linq;
+using Markdig.Extensions.CustomContainers;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
@@ -16,6 +17,7 @@ namespace osu.Game.Overlays.Wiki.Markdown
public class WikiMarkdownContainer : OsuMarkdownContainer
{
protected override bool Footnotes => true;
+ protected override bool CustomContainers => true;
public string CurrentPath
{
@@ -26,6 +28,11 @@ namespace osu.Game.Overlays.Wiki.Markdown
{
switch (markdownObject)
{
+ case CustomContainer:
+ // infoboxes are parsed into CustomContainer objects, but we don't have support for infoboxes yet.
+ // todo: add support for infobox.
+ break;
+
case YamlFrontMatterBlock yamlFrontMatterBlock:
container.Add(new WikiNoticeContainer(yamlFrontMatterBlock));
break;
diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
index f6abf259e8..f345504ca1 100644
--- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
+++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs
@@ -46,6 +46,10 @@ namespace osu.Game.Replays.Legacy
[IgnoreMember]
public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2);
+ [JsonIgnore]
+ [IgnoreMember]
+ public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke);
+
[Key(3)]
public ReplayButtonState ButtonState;
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index 6d7706cde2..d58a901154 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -12,7 +11,6 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.OpenGL.Vertices;
@@ -20,6 +18,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
+using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
@@ -84,8 +83,6 @@ namespace osu.Game.Rulesets.Mods
flashlight.Combo.BindTo(Combo);
drawableRuleset.KeyBindingInputManager.Add(flashlight);
-
- flashlight.Breaks = drawableRuleset.Beatmap.Breaks;
}
protected abstract Flashlight CreateFlashlight();
@@ -100,8 +97,6 @@ namespace osu.Game.Rulesets.Mods
public override bool RemoveCompletedTransforms => false;
- public List Breaks = new List();
-
private readonly float defaultFlashlightSize;
private readonly float sizeMultiplier;
private readonly bool comboBasedSize;
@@ -119,46 +114,50 @@ namespace osu.Game.Rulesets.Mods
shader = shaderManager.Load("PositionAndColour", FragmentShader);
}
+ [Resolved]
+ private Player? player { get; set; }
+
+ private readonly IBindable isBreakTime = new BindableBool();
+
protected override void LoadComplete()
{
base.LoadComplete();
- Combo.ValueChanged += OnComboChange;
+ Combo.ValueChanged += _ => UpdateFlashlightSize(GetSize());
- using (BeginAbsoluteSequence(0))
+ if (player != null)
{
- foreach (var breakPeriod in Breaks)
- {
- if (!breakPeriod.HasEffect)
- continue;
-
- if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
-
- this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
- this.Delay(breakPeriod.EndTime - FLASHLIGHT_FADE_DURATION).FadeInFromZero(FLASHLIGHT_FADE_DURATION);
- }
+ isBreakTime.BindTo(player.IsBreakTime);
+ isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true);
}
}
- protected abstract void OnComboChange(ValueChangedEvent e);
+ protected abstract void UpdateFlashlightSize(float size);
protected abstract string FragmentShader { get; }
- protected float GetSizeFor(int combo)
+ protected float GetSize()
{
float size = defaultFlashlightSize * sizeMultiplier;
- if (comboBasedSize)
- {
- if (combo >= 200)
- size *= 0.8f;
- else if (combo >= 100)
- size *= 0.9f;
- }
+ if (isBreakTime.Value)
+ size *= 2.5f;
+ else if (comboBasedSize)
+ size *= GetComboScaleFor(Combo.Value);
return size;
}
+ protected virtual float GetComboScaleFor(int combo)
+ {
+ if (combo >= 200)
+ return 0.625f;
+ if (combo >= 100)
+ return 0.8125f;
+
+ return 1.0f;
+ }
+
private Vector2 flashlightPosition;
protected Vector2 FlashlightPosition
@@ -201,6 +200,20 @@ namespace osu.Game.Rulesets.Mods
}
}
+ private float flashlightSmoothness = 1.1f;
+
+ public float FlashlightSmoothness
+ {
+ get => flashlightSmoothness;
+ set
+ {
+ if (flashlightSmoothness == value) return;
+
+ flashlightSmoothness = value;
+ Invalidate(Invalidation.DrawNode);
+ }
+ }
+
private class FlashlightDrawNode : DrawNode
{
protected new Flashlight Source => (Flashlight)base.Source;
@@ -210,6 +223,7 @@ namespace osu.Game.Rulesets.Mods
private Vector2 flashlightPosition;
private Vector2 flashlightSize;
private float flashlightDim;
+ private float flashlightSmoothness;
private IVertexBatch? quadBatch;
private Action? addAction;
@@ -228,6 +242,7 @@ namespace osu.Game.Rulesets.Mods
flashlightPosition = Vector2Extensions.Transform(Source.FlashlightPosition, DrawInfo.Matrix);
flashlightSize = Source.FlashlightSize * DrawInfo.Matrix.ExtractScale().Xy;
flashlightDim = Source.FlashlightDim;
+ flashlightSmoothness = Source.flashlightSmoothness;
}
public override void Draw(IRenderer renderer)
@@ -249,6 +264,7 @@ namespace osu.Game.Rulesets.Mods
shader.GetUniform("flashlightPos").UpdateValue(ref flashlightPosition);
shader.GetUniform("flashlightSize").UpdateValue(ref flashlightSize);
shader.GetUniform("flashlightDim").UpdateValue(ref flashlightDim);
+ shader.GetUniform("flashlightSmoothness").UpdateValue(ref flashlightSmoothness);
renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction);
diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
index dec81d9bbd..c2709db747 100644
--- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs
+++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -40,14 +38,21 @@ namespace osu.Game.Rulesets.Objects
for (int i = 0; i < timingPoints.Count; i++)
{
TimingControlPoint currentTimingPoint = timingPoints[i];
+ EffectControlPoint currentEffectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTimingPoint.Time);
int currentBeat = 0;
- // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
- double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
+ // Stop on the next timing point, or if there is no next timing point stop slightly past the last object
+ double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
+ double startTime = currentTimingPoint.Time;
double barLength = currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
- for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
+ if (currentEffectPoint.OmitFirstBarLine)
+ {
+ startTime += barLength;
+ }
+
+ for (double t = startTime; Precision.AlmostBigger(endTime, t); t += barLength, currentBeat++)
{
double roundedTime = Math.Round(t, MidpointRounding.AwayFromZero);
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index d20e0616e5..0f79e58201 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using Newtonsoft.Json;
@@ -198,6 +199,21 @@ namespace osu.Game.Rulesets.Objects
///
[NotNull]
protected virtual HitWindows CreateHitWindows() => new HitWindows();
+
+ public IList CreateSlidingSamples()
+ {
+ var slidingSamples = new List();
+
+ var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
+ if (normalSample != null)
+ slidingSamples.Add(normalSample.With("sliderslide"));
+
+ var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
+ if (whistleSample != null)
+ slidingSamples.Add(whistleSample.With("sliderwhistle"));
+
+ return slidingSamples;
+ }
}
public static class HitObjectExtensions
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 1a97153f2f..64ac021204 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -230,9 +230,9 @@ namespace osu.Game.Rulesets.UI
{
}
- protected override void ReloadMappings()
+ protected override void ReloadMappings(IQueryable realmKeyBindings)
{
- base.ReloadMappings();
+ base.ReloadMappings(realmKeyBindings);
KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index d0aca4e7fc..37da157cc1 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -5,10 +5,10 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Layout;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -127,6 +127,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight;
+ public override void Add(HitObjectLifetimeEntry entry)
+ {
+ // Scroll info is not available until loaded.
+ // The lifetime of all entries will be updated in the first Update.
+ if (IsLoaded)
+ setComputedLifetimeStart(entry);
+
+ base.Add(entry);
+ }
+
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
base.AddDrawable(entry, drawable);
@@ -145,7 +155,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
private void invalidateHitObject(DrawableHitObject hitObject)
{
- hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
layoutComputed.Remove(hitObject);
}
@@ -157,10 +166,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
layoutComputed.Clear();
- // Reset lifetime to the conservative estimation.
- // If a drawable becomes alive by this lifetime, its lifetime will be updated to a more precise lifetime in the next update.
foreach (var entry in Entries)
- entry.SetInitialLifetime();
+ setComputedLifetimeStart(entry);
scrollingInfo.Algorithm.Reset();
@@ -187,38 +194,46 @@ namespace osu.Game.Rulesets.UI.Scrolling
}
}
- private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
+ ///
+ /// Get a conservative maximum bounding box of a corresponding to .
+ /// It is used to calculate when the hit object appears.
+ ///
+ protected virtual RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry) => new RectangleF().Inflate(100);
+
+ private double computeDisplayStartTime(HitObjectLifetimeEntry entry)
{
- // Origin position may be relative to the parent size
- Debug.Assert(hitObject.Parent != null);
+ RectangleF boundingBox = GetConservativeBoundingBox(entry);
+ float startOffset = 0;
- float originAdjustment = 0.0f;
-
- // calculate the dimension of the part of the hitobject that should already be visible
- // when the hitobject origin first appears inside the scrolling container
switch (direction.Value)
{
- case ScrollingDirection.Up:
- originAdjustment = hitObject.OriginPosition.Y;
+ case ScrollingDirection.Right:
+ startOffset = boundingBox.Right;
break;
case ScrollingDirection.Down:
- originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y;
+ startOffset = boundingBox.Bottom;
break;
case ScrollingDirection.Left:
- originAdjustment = hitObject.OriginPosition.X;
+ startOffset = -boundingBox.Left;
break;
- case ScrollingDirection.Right:
- originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X;
+ case ScrollingDirection.Up:
+ startOffset = -boundingBox.Top;
break;
}
- double computedStartTime = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
+ return scrollingInfo.Algorithm.GetDisplayStartTime(entry.HitObject.StartTime, startOffset, timeRange.Value, scrollLength);
+ }
+
+ private void setComputedLifetimeStart(HitObjectLifetimeEntry entry)
+ {
+ double computedStartTime = computeDisplayStartTime(entry);
// always load the hitobject before its first judgement offset
- return Math.Min(hitObject.HitObject.StartTime - hitObject.MaximumJudgementOffset, computedStartTime);
+ double judgementOffset = entry.HitObject.HitWindows?.WindowFor(Scoring.HitResult.Miss) ?? 0;
+ entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
}
private void updateLayoutRecursive(DrawableHitObject hitObject)
@@ -236,8 +251,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
updateLayoutRecursive(obj);
- // Nested hitobjects don't need to scroll, but they do need accurate positions
+ // Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime
updatePosition(obj, hitObject.HitObject.StartTime);
+ setComputedLifetimeStart(obj.Entry);
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
index 078f06b745..34e5b7f9de 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time);
- protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
+ protected sealed override HitObjectContainer CreateHitObjectContainer() => CreateScrollingHitObjectContainer();
+
+ protected virtual ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new ScrollingHitObjectContainer();
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 8b38d9c612..43ad270c16 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -3,7 +3,6 @@
#nullable disable
-using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
@@ -13,7 +12,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@@ -61,25 +59,31 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
case NotifyCollectionChangedAction.Add:
foreach (object o in args.NewItems)
- SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
+ {
+ if (blueprintMap.TryGetValue((T)o, out var blueprint))
+ blueprint.Select();
+ }
break;
case NotifyCollectionChangedAction.Remove:
foreach (object o in args.OldItems)
- SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
+ {
+ if (blueprintMap.TryGetValue((T)o, out var blueprint))
+ blueprint.Deselect();
+ }
break;
}
};
SelectionHandler = CreateSelectionHandler();
- SelectionHandler.DeselectAll = deselectAll;
+ SelectionHandler.DeselectAll = DeselectAll;
SelectionHandler.SelectedItems.BindTo(SelectedItems);
AddRangeInternal(new[]
{
- DragBox = CreateDragBox(selectBlueprintsFromDragRectangle),
+ DragBox = CreateDragBox(),
SelectionHandler,
SelectionBlueprints = CreateSelectionBlueprintContainer(),
SelectionHandler.CreateProxy(),
@@ -101,12 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[CanBeNull]
protected virtual SelectionBlueprint CreateBlueprintFor(T item) => null;
- protected virtual DragBox CreateDragBox(Action performSelect) => new DragBox(performSelect);
-
- ///
- /// Whether this component is in a state where items outside a drag selection should be deselected. If false, selection will only be added to.
- ///
- protected virtual bool AllowDeselectionDuringDrag => true;
+ protected virtual DragBox CreateDragBox() => new DragBox();
protected override bool OnMouseDown(MouseDownEvent e)
{
@@ -142,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (endClickSelection(e) || ClickedBlueprint != null)
return true;
- deselectAll();
+ DeselectAll();
return true;
}
@@ -171,11 +170,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
finishSelectionMovement();
}
+ private MouseButtonEvent lastDragEvent;
+
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button == MouseButton.Right)
return false;
+ lastDragEvent = e;
+
if (movementBlueprints != null)
{
isDraggingBlueprint = true;
@@ -183,30 +186,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
return true;
}
- if (DragBox.HandleDrag(e))
- {
- DragBox.Show();
- return true;
- }
-
- return false;
+ DragBox.HandleDrag(e);
+ DragBox.Show();
+ return true;
}
protected override void OnDrag(DragEvent e)
{
- if (e.Button == MouseButton.Right)
- return;
-
- if (DragBox.State == Visibility.Visible)
- DragBox.HandleDrag(e);
+ lastDragEvent = e;
moveCurrentSelection(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
- if (e.Button == MouseButton.Right)
- return;
+ lastDragEvent = null;
if (isDraggingBlueprint)
{
@@ -214,8 +208,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
changeHandler?.EndChange();
}
- if (DragBox.State == Visibility.Visible)
- DragBox.Hide();
+ DragBox.Hide();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (lastDragEvent != null && DragBox.State == Visibility.Visible)
+ {
+ lastDragEvent.Target = this;
+ DragBox.HandleDrag(lastDragEvent);
+ UpdateSelectionFromDragBox();
+ }
}
///
@@ -233,7 +238,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (!SelectionHandler.SelectedBlueprints.Any())
return false;
- deselectAll();
+ DeselectAll();
return true;
}
@@ -380,44 +385,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
///
- /// Select all masks in a given rectangle selection area.
+ /// Select all blueprints in a selection area specified by .
///
- /// The rectangle to perform a selection on in screen-space coordinates.
- private void selectBlueprintsFromDragRectangle(RectangleF rect)
+ protected virtual void UpdateSelectionFromDragBox()
{
+ var quad = DragBox.Box.ScreenSpaceDrawQuad;
+
foreach (var blueprint in SelectionBlueprints)
{
- // only run when utmost necessary to avoid unnecessary rect computations.
- bool isValidForSelection() => blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint);
-
switch (blueprint.State)
{
- case SelectionState.NotSelected:
- if (isValidForSelection())
- blueprint.Select();
+ case SelectionState.Selected:
+ // Selection is preserved even after blueprint becomes dead.
+ if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint))
+ blueprint.Deselect();
break;
- case SelectionState.Selected:
- if (AllowDeselectionDuringDrag && !isValidForSelection())
- blueprint.Deselect();
+ case SelectionState.NotSelected:
+ if (blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint))
+ blueprint.Select();
break;
}
}
}
///
- /// Selects all s.
+ /// Select all currently-present items.
///
- protected virtual void SelectAll()
- {
- // Scheduled to allow the change in lifetime to take place.
- Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select()));
- }
+ protected abstract void SelectAll();
///
- /// Deselects all selected s.
+ /// Deselect all selected items.
///
- private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
+ protected void DeselectAll() => SelectedItems.Clear();
protected virtual void OnBlueprintSelected(SelectionBlueprint blueprint)
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 4c37d200bc..ec07da43a0 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -12,7 +12,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
@@ -37,7 +36,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
private PlacementBlueprint currentPlacement;
- private InputManager inputManager;
///
/// Positional input must be received outside the container's bounds,
@@ -66,8 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
- inputManager = GetContainingInputManager();
-
Beatmap.HitObjectAdded += hitObjectAdded;
// updates to selected are handled for us by SelectionHandler.
@@ -220,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementPosition()
{
- var snapResult = Composer.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
+ var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position);
// if no time was found from positional snapping, we should still quantize to the beat.
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);
diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
index 838562719d..905d47533a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs
@@ -8,7 +8,6 @@ using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
@@ -21,18 +20,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
public class DragBox : CompositeDrawable, IStateful
{
- protected readonly Action PerformSelection;
-
- protected Drawable Box;
+ public Drawable Box { get; private set; }
///
/// Creates a new .
///
- /// A delegate that performs drag selection.
- public DragBox(Action performSelection)
+ public DragBox()
{
- PerformSelection = performSelection;
-
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
Alpha = 0;
@@ -46,30 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected virtual Drawable CreateBox() => new BoxWithBorders();
- private RectangleF? dragRectangle;
-
///
/// Handle a forwarded mouse event.
///
/// The mouse event.
- /// Whether the event should be handled and blocking.
- public virtual bool HandleDrag(MouseButtonEvent e)
+ public virtual void HandleDrag(MouseButtonEvent e)
{
- var dragPosition = e.ScreenSpaceMousePosition;
- var dragStartPosition = e.ScreenSpaceMouseDownPosition;
-
- var dragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y);
-
- // We use AABBFloat instead of RectangleF since it handles negative sizes for us
- var rec = dragQuad.AABBFloat;
- dragRectangle = rec;
-
- var topLeft = ToLocalSpace(rec.TopLeft);
- var bottomRight = ToLocalSpace(rec.BottomRight);
-
- Box.Position = topLeft;
- Box.Size = bottomRight - topLeft;
- return true;
+ Box.Position = Vector2.ComponentMin(e.MouseDownPosition, e.MousePosition);
+ Box.Size = Vector2.ComponentMax(e.MouseDownPosition, e.MousePosition) - Box.Position;
}
private Visibility state;
@@ -87,19 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
- protected override void Update()
- {
- base.Update();
-
- if (dragRectangle != null)
- PerformSelection?.Invoke(dragRectangle.Value);
- }
-
- public override void Hide()
- {
- State = Visibility.Hidden;
- dragRectangle = null;
- }
+ public override void Hide() => State = Visibility.Hidden;
public override void Show() => State = Visibility.Visible;
diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs
index 6a4fe27f04..7423b368b4 100644
--- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs
@@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -27,6 +28,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private HitObjectUsageEventBuffer usageEventBuffer;
+ protected InputManager InputManager { get; private set; }
+
protected EditorBlueprintContainer(HitObjectComposer composer)
{
Composer = composer;
@@ -42,6 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
+ InputManager = GetContainingInputManager();
+
Beatmap.HitObjectAdded += AddBlueprintFor;
Beatmap.HitObjectRemoved += RemoveBlueprintFor;
@@ -66,8 +71,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints)
=> blueprints.OrderBy(b => b.Item.StartTime);
- protected override bool AllowDeselectionDuringDrag => !EditorClock.IsRunning;
-
protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result)
{
if (!base.ApplySnapResult(blueprints, result))
@@ -133,8 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void SelectAll()
{
Composer.Playfield.KeepAllAlive();
-
- base.SelectAll();
+ SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray());
}
protected override void OnBlueprintSelected(SelectionBlueprint blueprint)
diff --git a/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs
new file mode 100644
index 0000000000..58bfaf56ff
--- /dev/null
+++ b/osu.Game/Screens/Edit/Compose/Components/ScrollingDragBox.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+
+namespace osu.Game.Screens.Edit.Compose.Components
+{
+ ///
+ /// A that scrolls along with the scrolling playfield.
+ ///
+ public class ScrollingDragBox : DragBox
+ {
+ public double MinTime { get; private set; }
+
+ public double MaxTime { get; private set; }
+
+ private double? startTime;
+
+ private readonly ScrollingPlayfield playfield;
+
+ public ScrollingDragBox(Playfield playfield)
+ {
+ this.playfield = playfield as ScrollingPlayfield ?? throw new ArgumentException("Playfield must be of type {nameof(ScrollingPlayfield)} to use this class.", nameof(playfield));
+ }
+
+ public override void HandleDrag(MouseButtonEvent e)
+ {
+ base.HandleDrag(e);
+
+ startTime ??= playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMouseDownPosition);
+ double endTime = playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMousePosition);
+
+ MinTime = Math.Min(startTime.Value, endTime);
+ MaxTime = Math.Max(startTime.Value, endTime);
+
+ var startPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(startTime.Value));
+ var endPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(endTime));
+
+ switch (playfield.ScrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Up:
+ case ScrollingDirection.Down:
+ Box.Y = Math.Min(startPos.Y, endPos.Y);
+ Box.Height = Math.Max(startPos.Y, endPos.Y) - Box.Y;
+ break;
+
+ case ScrollingDirection.Left:
+ case ScrollingDirection.Right:
+ Box.X = Math.Min(startPos.X, endPos.X);
+ Box.Width = Math.Max(startPos.X, endPos.X) - Box.X;
+ break;
+ }
+ }
+
+ public override void Hide()
+ {
+ base.Hide();
+ startTime = null;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 8419d3b380..269c19f846 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -305,7 +305,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected void DeleteSelected()
{
- DeleteItems(selectedBlueprints.Select(b => b.Item));
+ DeleteItems(SelectedItems.ToArray());
}
#endregion
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 721f0c4e3b..a73ada76f5 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -304,10 +304,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
///
public double VisibleRange => editorClock.TrackLength / Zoom;
- public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) =>
- new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
+ public double TimeAtPosition(float x)
+ {
+ return x / Content.DrawWidth * editorClock.TrackLength;
+ }
- private double getTimeFromPosition(Vector2 localPosition) =>
- (localPosition.X / Content.DrawWidth) * editorClock.TrackLength;
+ public float PositionAtTime(double time)
+ {
+ return (float)(time / editorClock.TrackLength * Content.DrawWidth);
+ }
+
+ public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
+ {
+ double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);
+ return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time));
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 590f92d281..b79c2675c8 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -3,7 +3,6 @@
#nullable disable
-using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -13,7 +12,6 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
@@ -31,10 +29,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved(CanBeNull = true)]
private Timeline timeline { get; set; }
- private DragEvent lastDragEvent;
private Bindable placement;
private SelectionBlueprint placementBlueprint;
+ private bool hitObjectDragged;
+
///
/// Positional input must be received outside the container's bounds,
/// in order to handle timeline blueprints which are stacked offscreen.
@@ -65,7 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void LoadComplete()
{
base.LoadComplete();
- DragBox.Alpha = 0;
placement = Beatmap.PlacementObject.GetBoundCopy();
placement.ValueChanged += placementChanged;
@@ -93,24 +91,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override Container> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
- protected override void OnDrag(DragEvent e)
+ protected override bool OnDragStart(DragStartEvent e)
{
- handleScrollViaDrag(e);
+ if (!base.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition))
+ return false;
- base.OnDrag(e);
- }
-
- protected override void OnDragEnd(DragEndEvent e)
- {
- base.OnDragEnd(e);
- lastDragEvent = null;
+ return base.OnDragStart(e);
}
protected override void Update()
{
- // trigger every frame so drags continue to update selection while playback is scrolling the timeline.
- if (lastDragEvent != null)
- OnDrag(lastDragEvent);
+ if (IsDragged || hitObjectDragged)
+ handleScrollViaDrag();
if (Composer != null && timeline != null)
{
@@ -165,30 +157,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
return new TimelineHitObjectBlueprint(item)
{
- OnDragHandled = handleScrollViaDrag,
+ OnDragHandled = e => hitObjectDragged = e != null,
};
}
- protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect);
+ protected sealed override DragBox CreateDragBox() => new TimelineDragBox();
- private void handleScrollViaDrag(DragEvent e)
+ protected override void UpdateSelectionFromDragBox()
{
- lastDragEvent = e;
+ var dragBox = (TimelineDragBox)DragBox;
+ double minTime = dragBox.MinTime;
+ double maxTime = dragBox.MaxTime;
- if (lastDragEvent == null)
- return;
+ SelectedItems.RemoveAll(hitObject => !shouldBeSelected(hitObject));
- if (timeline != null)
+ foreach (var hitObject in Beatmap.HitObjects.Except(SelectedItems).Where(shouldBeSelected))
{
- var timelineQuad = timeline.ScreenSpaceDrawQuad;
- float mouseX = e.ScreenSpaceMousePosition.X;
-
- // scroll if in a drag and dragging outside visible extents
- if (mouseX > timelineQuad.TopRight.X)
- timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
- else if (mouseX < timelineQuad.TopLeft.X)
- timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
+ Composer.Playfield.SetKeepAlive(hitObject, true);
+ SelectedItems.Add(hitObject);
}
+
+ bool shouldBeSelected(HitObject hitObject)
+ {
+ double midTime = (hitObject.StartTime + hitObject.GetEndTime()) / 2;
+ return minTime <= midTime && midTime <= maxTime;
+ }
+ }
+
+ private void handleScrollViaDrag()
+ {
+ if (timeline == null) return;
+
+ var timelineQuad = timeline.ScreenSpaceDrawQuad;
+ float mouseX = InputManager.CurrentState.Mouse.Position.X;
+
+ // scroll if in a drag and dragging outside visible extents
+ if (mouseX > timelineQuad.TopRight.X)
+ timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
+ else if (mouseX < timelineQuad.TopLeft.X)
+ timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
}
private class SelectableAreaBackground : CompositeDrawable
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs
index c026c169d6..65d9293b7e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs
@@ -6,76 +6,44 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
-using osu.Framework.Utils;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineDragBox : DragBox
{
- // the following values hold the start and end X positions of the drag box in the timeline's local space,
- // but with zoom unapplied in order to be able to compensate for positional changes
- // while the timeline is being zoomed in/out.
- private float? selectionStart;
- private float selectionEnd;
+ public double MinTime { get; private set; }
+
+ public double MaxTime { get; private set; }
+
+ private double? startTime;
[Resolved]
private Timeline timeline { get; set; }
- public TimelineDragBox(Action performSelect)
- : base(performSelect)
- {
- }
-
protected override Drawable CreateBox() => new Box
{
RelativeSizeAxes = Axes.Y,
Alpha = 0.3f
};
- public override bool HandleDrag(MouseButtonEvent e)
+ public override void HandleDrag(MouseButtonEvent e)
{
- // The dragbox should only be active if the mouseDownPosition.Y is within this drawable's bounds.
- float localY = ToLocalSpace(e.ScreenSpaceMouseDownPosition).Y;
- if (DrawRectangle.Top > localY || DrawRectangle.Bottom < localY)
- return false;
+ startTime ??= timeline.TimeAtPosition(e.MouseDownPosition.X);
+ double endTime = timeline.TimeAtPosition(e.MousePosition.X);
- selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom;
+ MinTime = Math.Min(startTime.Value, endTime);
+ MaxTime = Math.Max(startTime.Value, endTime);
- // only calculate end when a transition is not in progress to avoid bouncing.
- if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom))
- selectionEnd = e.MousePosition.X / timeline.CurrentZoom;
-
- updateDragBoxPosition();
- return true;
- }
-
- private void updateDragBoxPosition()
- {
- if (selectionStart == null)
- return;
-
- float rescaledStart = selectionStart.Value * timeline.CurrentZoom;
- float rescaledEnd = selectionEnd * timeline.CurrentZoom;
-
- Box.X = Math.Min(rescaledStart, rescaledEnd);
- Box.Width = Math.Abs(rescaledStart - rescaledEnd);
-
- var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat;
-
- // we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment.
- boxScreenRect.Y -= boxScreenRect.Height;
- boxScreenRect.Height *= 2;
-
- PerformSelection?.Invoke(boxScreenRect);
+ Box.X = timeline.PositionAtTime(MinTime);
+ Box.Width = timeline.PositionAtTime(MaxTime) - Box.X;
}
public override void Hide()
{
base.Hide();
- selectionStart = null;
+ startTime = null;
}
}
}
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
index 16c0064e80..839535b99f 100644
--- a/osu.Game/Screens/Edit/EditorBeatmap.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -352,6 +352,8 @@ namespace osu.Game.Screens.Edit
var updates = batchPendingUpdates.ToArray();
batchPendingUpdates.Clear();
+ foreach (var h in deletes) SelectedHitObjects.Remove(h);
+
foreach (var h in deletes) HitObjectRemoved?.Invoke(h);
foreach (var h in inserts) HitObjectAdded?.Invoke(h);
foreach (var h in updates) HitObjectUpdated?.Invoke(h);
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
index fde895a1ca..48908fb9a0 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
@@ -124,7 +124,7 @@ namespace osu.Game.Screens.Play.HUD
float fadeBottom = scroll.Current + scroll.DrawHeight;
float fadeTop = scroll.Current + panel_height;
- if (scroll.Current <= 0) fadeTop -= panel_height;
+ if (scroll.IsScrolledToStart()) fadeTop -= panel_height;
if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height;
// logic is mostly shared with Leaderboard, copied here for simplicity.
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
index 29354e610d..2eec8253b3 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -39,8 +40,6 @@ namespace osu.Game.Screens.Play.HUD
private const float rank_text_width = 35f;
- private const float score_components_width = 85f;
-
private const float avatar_size = 25f;
private const double panel_transition_duration = 500;
@@ -161,7 +160,7 @@ namespace osu.Game.Screens.Play.HUD
{
new Dimension(GridSizeMode.Absolute, rank_text_width),
new Dimension(),
- new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width),
+ new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@@ -286,8 +285,19 @@ namespace osu.Game.Screens.Play.HUD
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true);
- Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true);
- Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true);
+
+ Accuracy.BindValueChanged(v =>
+ {
+ accuracyText.Text = v.NewValue.FormatAccuracy();
+ updateDetailsWidth();
+ }, true);
+
+ Combo.BindValueChanged(v =>
+ {
+ comboText.Text = $"{v.NewValue}x";
+ updateDetailsWidth();
+ }, true);
+
HasQuit.BindValueChanged(_ => updateState());
}
@@ -303,13 +313,10 @@ namespace osu.Game.Screens.Play.HUD
private void changeExpandedState(ValueChangedEvent expanded)
{
- scoreComponents.ClearTransforms();
-
if (expanded.NewValue)
{
gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint);
- scoreComponents.ResizeWidthTo(score_components_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint);
usernameText.FadeIn(panel_transition_duration, Easing.OutQuint);
@@ -318,11 +325,29 @@ namespace osu.Game.Screens.Play.HUD
{
gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint);
- scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint);
usernameText.FadeOut(text_transition_duration, Easing.OutQuint);
}
+
+ updateDetailsWidth();
+ }
+
+ private float? scoreComponentsTargetWidth;
+
+ private void updateDetailsWidth()
+ {
+ const float score_components_min_width = 88f;
+
+ float newWidth = Expanded.Value
+ ? Math.Max(score_components_min_width, comboText.DrawWidth + accuracyText.DrawWidth + 25)
+ : 0;
+
+ if (scoreComponentsTargetWidth == newWidth)
+ return;
+
+ scoreComponentsTargetWidth = newWidth;
+ scoreComponents.ResizeWidthTo(newWidth, panel_transition_duration, Easing.OutQuint);
}
private void updateState()
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 3fbb051c3b..7833c2d7fa 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -39,6 +39,10 @@ namespace osu.Game.Screens.Play
///
public float BottomScoringElementsHeight { get; private set; }
+ // HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
+ // Without blocking input, this would also allow them to be interacted with in such a state.
+ public override bool PropagatePositionalInputSubTree => ShowHud.Value;
+
public readonly KeyCounterDisplay KeyCounter;
public readonly ModDisplay ModDisplay;
public readonly HoldForMenuButton HoldToQuit;
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index be77304076..d56b9c23c8 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -76,6 +76,7 @@ namespace osu.Game.Screens.Play
req.Success += r =>
{
+ Logger.Log($"Score submission token retrieved ({r.ID})");
token = r.ID;
tcs.SetResult(true);
};
@@ -91,6 +92,11 @@ namespace osu.Game.Screens.Play
void handleTokenFailure(Exception exception)
{
+ // This method may be invoked multiple times due to the Task.Wait call above.
+ // We only really care about the first error.
+ if (!tcs.TrySetResult(false))
+ return;
+
if (HandleTokenRetrievalFailure(exception))
{
if (string.IsNullOrEmpty(exception.Message))
@@ -104,8 +110,12 @@ namespace osu.Game.Screens.Play
this.Exit();
});
}
-
- tcs.SetResult(false);
+ else
+ {
+ // Gameplay is allowed to continue, but we still should keep track of the error.
+ // In the future, this should be visible to the user in some way.
+ Logger.Log($"Score submission token retrieval failed ({exception.Message})");
+ }
}
}
diff --git a/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs b/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs
index 3c4ed4734b..023d3627b0 100644
--- a/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs
+++ b/osu.Game/Screens/Select/Carousel/UpdateBeatmapSetButton.cs
@@ -2,12 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -43,11 +45,15 @@ namespace osu.Game.Screens.Select.Carousel
Origin = Anchor.CentreLeft;
}
+ private Bindable preferNoVideo = null!;
+
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
const float icon_size = 14;
+ preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo);
+
Content.Anchor = Anchor.CentreLeft;
Content.Origin = Anchor.CentreLeft;
@@ -104,7 +110,7 @@ namespace osu.Game.Screens.Select.Carousel
return;
}
- beatmapDownloader.DownloadAsUpdate(beatmapSetInfo);
+ beatmapDownloader.DownloadAsUpdate(beatmapSetInfo, preferNoVideo.Value);
attachExistingDownload();
};
}
diff --git a/osu.Game/Screens/Select/FooterButtonRandom.cs b/osu.Game/Screens/Select/FooterButtonRandom.cs
index 1f56915f62..aad7fdff39 100644
--- a/osu.Game/Screens/Select/FooterButtonRandom.cs
+++ b/osu.Game/Screens/Select/FooterButtonRandom.cs
@@ -138,7 +138,8 @@ namespace osu.Game.Screens.Select
return false;
}
- TriggerClick();
+ if (!e.Repeat)
+ TriggerClick();
return true;
}
diff --git a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs
index c9d4dc7811..bacaccd68e 100644
--- a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs
+++ b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs
@@ -261,8 +261,8 @@ namespace osu.Game.Screens.Utility
string exclusive = "unknown";
- if (host.Window is WindowsWindow windowsWindow)
- exclusive = windowsWindow.FullscreenCapability.ToString();
+ if (host.Renderer is IWindowsRenderer windowsRenderer)
+ exclusive = windowsRenderer.FullscreenCapability.ToString();
statusText.Clear();
diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs
index 5a1ef34151..2937b62eec 100644
--- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs
+++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs
@@ -117,6 +117,11 @@ namespace osu.Game.Skinning.Editor
return false;
}
+ protected override void SelectAll()
+ {
+ SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray());
+ }
+
///
/// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints).
///
diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs
deleted file mode 100644
index 586882d790..0000000000
--- a/osu.Game/Skinning/HUDSkinComponents.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-namespace osu.Game.Skinning
-{
- public enum HUDSkinComponents
- {
- ComboCounter,
- ScoreCounter,
- AccuracyCounter,
- HealthDisplay,
- SongProgress,
- BarHitErrorMeter,
- ColourHitErrorMeter,
- }
-}
diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
index 45454be4a5..3ec0ee6006 100644
--- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs
@@ -1,21 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Skinning
{
+ ///
+ /// This class exists for the explicit purpose of ferrying information from ManiaBeatmap in a way LegacySkin can use it.
+ /// This is because half of the mania legacy skin implementation is in LegacySkin (osu.Game project) which doesn't have visibility
+ /// over ManiaBeatmap / StageDefinition.
+ ///
public class LegacyManiaSkinConfigurationLookup
{
- public readonly int Keys;
- public readonly LegacyManiaSkinConfigurationLookups Lookup;
- public readonly int? TargetColumn;
+ ///
+ /// Total columns across all stages.
+ ///
+ public readonly int TotalColumns;
- public LegacyManiaSkinConfigurationLookup(int keys, LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null)
+ ///
+ /// The column which is being looked up.
+ /// May be null if the configuration does not apply to a specific column.
+ /// Note that this is the absolute index across all stages.
+ ///
+ public readonly int? ColumnIndex;
+
+ public readonly LegacyManiaSkinConfigurationLookups Lookup;
+
+ public LegacyManiaSkinConfigurationLookup(int totalColumns, LegacyManiaSkinConfigurationLookups lookup, int? columnIndex = null)
{
- Keys = keys;
+ TotalColumns = totalColumns;
Lookup = lookup;
- TargetColumn = targetColumn;
+ ColumnIndex = columnIndex;
}
}
@@ -29,6 +42,8 @@ namespace osu.Game.Skinning
HitPosition,
ScorePosition,
LightPosition,
+ StagePaddingTop,
+ StagePaddingBottom,
HitTargetImage,
ShowJudgementLine,
KeyImage,
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 1e096702b3..646746a0f3 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -128,18 +128,18 @@ namespace osu.Game.Skinning
private IBindable? lookupForMania(LegacyManiaSkinConfigurationLookup maniaLookup)
{
- if (!maniaConfigurations.TryGetValue(maniaLookup.Keys, out var existing))
- maniaConfigurations[maniaLookup.Keys] = existing = new LegacyManiaSkinConfiguration(maniaLookup.Keys);
+ if (!maniaConfigurations.TryGetValue(maniaLookup.TotalColumns, out var existing))
+ maniaConfigurations[maniaLookup.TotalColumns] = existing = new LegacyManiaSkinConfiguration(maniaLookup.TotalColumns);
switch (maniaLookup.Lookup)
{
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
- Debug.Assert(maniaLookup.TargetColumn != null);
- return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value]));
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value]));
case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
- Debug.Assert(maniaLookup.TargetColumn != null);
- return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.TargetColumn.Value]));
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As(new Bindable(existing.ColumnSpacing[maniaLookup.ColumnIndex.Value]));
case LegacyManiaSkinConfigurationLookups.HitPosition:
return SkinUtils.As(new Bindable(existing.HitPosition));
@@ -157,15 +157,15 @@ namespace osu.Game.Skinning
return SkinUtils.As(getManiaImage(existing, "LightingN"));
case LegacyManiaSkinConfigurationLookups.ExplosionScale:
- Debug.Assert(maniaLookup.TargetColumn != null);
+ Debug.Assert(maniaLookup.ColumnIndex != null);
if (GetConfig(SkinConfiguration.LegacySetting.Version)?.Value < 2.5m)
return SkinUtils.As(new Bindable(1));
- if (existing.ExplosionWidth[maniaLookup.TargetColumn.Value] != 0)
- return SkinUtils.As(new Bindable(existing.ExplosionWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
+ if (existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] != 0)
+ return SkinUtils.As(new Bindable(existing.ExplosionWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
- return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
+ return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.ColumnIndex.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE));
case LegacyManiaSkinConfigurationLookups.ColumnLineColour:
return SkinUtils.As(getCustomColour(existing, "ColourColumnLine"));
@@ -174,53 +174,53 @@ namespace osu.Game.Skinning
return SkinUtils.As(getCustomColour(existing, "ColourJudgementLine"));
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
- Debug.Assert(maniaLookup.TargetColumn != null);
- return SkinUtils.As(getCustomColour(existing, $"Colour{maniaLookup.TargetColumn + 1}"));
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As(getCustomColour(existing, $"Colour{maniaLookup.ColumnIndex + 1}"));
case LegacyManiaSkinConfigurationLookups.ColumnLightColour:
- Debug.Assert(maniaLookup.TargetColumn != null);
- return SkinUtils.As(getCustomColour(existing, $"ColourLight{maniaLookup.TargetColumn + 1}"));
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As(getCustomColour(existing, $"ColourLight{maniaLookup.ColumnIndex + 1}"));
case LegacyManiaSkinConfigurationLookups.MinimumColumnWidth:
return SkinUtils.As(new Bindable(existing.MinimumColumnWidth));
case LegacyManiaSkinConfigurationLookups.NoteImage:
- Debug.Assert(maniaLookup.TargetColumn != null);
- return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}"));
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.ColumnIndex}"));
case LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage:
- Debug.Assert(maniaLookup.TargetColumn != null);
- return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}H"));
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.ColumnIndex}H"));
case LegacyManiaSkinConfigurationLookups.HoldNoteTailImage:
- Debug.Assert(maniaLookup.TargetColumn != null);
- return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}T"));
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.ColumnIndex}T"));
case LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage:
- Debug.Assert(maniaLookup.TargetColumn != null);
- return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}L"));
+ Debug.Assert(maniaLookup.ColumnIndex != null);
+ return SkinUtils.As