diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml
index 366f172c30..fe63f5faf3 100644
--- a/.idea/.idea.osu.Desktop/.idea/modules.xml
+++ b/.idea/.idea.osu.Desktop/.idea/modules.xml
@@ -2,7 +2,6 @@
-
diff --git a/global.json b/global.json
index 9aa5b6192b..233a040d18 100644
--- a/global.json
+++ b/global.json
@@ -5,6 +5,6 @@
"version": "3.1.100"
},
"msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.0.50"
+ "Microsoft.Build.Traversal": "2.0.52"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 493b1f5529..e5fed09c07 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs
index 2e5fa59d20..9839d16030 100644
--- a/osu.Android/OsuGameActivity.cs
+++ b/osu.Android/OsuGameActivity.cs
@@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.Android
{
- [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = true)]
+ [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index cd31df316a..2079f136d2 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -16,6 +16,7 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
+using osu.Desktop.Windows;
namespace osu.Desktop
{
@@ -98,6 +99,9 @@ namespace osu.Desktop
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
LoadComponentAsync(new DiscordRichPresence(), Add);
+
+ if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
+ LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
}
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
new file mode 100644
index 0000000000..86174ceb90
--- /dev/null
+++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Game.Configuration;
+
+namespace osu.Desktop.Windows
+{
+ public class GameplayWinKeyBlocker : Component
+ {
+ private Bindable allowScreenSuspension;
+ private Bindable disableWinKey;
+
+ private GameHost host;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuConfigManager config)
+ {
+ this.host = host;
+
+ allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
+ allowScreenSuspension.BindValueChanged(_ => updateBlocking());
+
+ disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey);
+ disableWinKey.BindValueChanged(_ => updateBlocking(), true);
+ }
+
+ private void updateBlocking()
+ {
+ bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
+
+ if (shouldDisable)
+ host.InputThread.Scheduler.Add(WindowsKey.Disable);
+ else
+ host.InputThread.Scheduler.Add(WindowsKey.Enable);
+ }
+ }
+}
diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs
new file mode 100644
index 0000000000..f19d741107
--- /dev/null
+++ b/osu.Desktop/Windows/WindowsKey.cs
@@ -0,0 +1,80 @@
+// 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.Runtime.InteropServices;
+
+namespace osu.Desktop.Windows
+{
+ internal class WindowsKey
+ {
+ private delegate int LowLevelKeyboardProcDelegate(int nCode, int wParam, ref KdDllHookStruct lParam);
+
+ private static bool isBlocked;
+
+ private const int wh_keyboard_ll = 13;
+ private const int wm_keydown = 256;
+ private const int wm_syskeyup = 261;
+
+ //Resharper disable once NotAccessedField.Local
+ private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC
+ private static IntPtr keyHook;
+
+ [StructLayout(LayoutKind.Explicit)]
+ private readonly struct KdDllHookStruct
+ {
+ [FieldOffset(0)]
+ public readonly int VkCode;
+
+ [FieldOffset(8)]
+ public readonly int Flags;
+ }
+
+ private static int lowLevelKeyboardProc(int nCode, int wParam, ref KdDllHookStruct lParam)
+ {
+ if (wParam >= wm_keydown && wParam <= wm_syskeyup)
+ {
+ switch (lParam.VkCode)
+ {
+ case 0x5B: // left windows key
+ case 0x5C: // right windows key
+ return 1;
+ }
+ }
+
+ return callNextHookEx(0, nCode, wParam, ref lParam);
+ }
+
+ internal static void Disable()
+ {
+ if (keyHook != IntPtr.Zero || isBlocked)
+ return;
+
+ keyHook = setWindowsHookEx(wh_keyboard_ll, (keyboardHookDelegate = lowLevelKeyboardProc), Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
+
+ isBlocked = true;
+ }
+
+ internal static void Enable()
+ {
+ if (keyHook == IntPtr.Zero || !isBlocked)
+ return;
+
+ keyHook = unhookWindowsHookEx(keyHook);
+ keyboardHookDelegate = null;
+
+ keyHook = IntPtr.Zero;
+
+ isBlocked = false;
+ }
+
+ [DllImport(@"user32.dll", EntryPoint = @"SetWindowsHookExA")]
+ private static extern IntPtr setWindowsHookEx(int idHook, LowLevelKeyboardProcDelegate lpfn, IntPtr hMod, int dwThreadId);
+
+ [DllImport(@"user32.dll", EntryPoint = @"UnhookWindowsHookEx")]
+ private static extern IntPtr unhookWindowsHookEx(IntPtr hHook);
+
+ [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")]
+ private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam);
+ }
+}
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 41e726e05c..ff26f4afaa 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
index f4749be370..df54df7b01 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs
@@ -8,7 +8,6 @@ using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
@@ -83,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public float Position
{
- get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position;
+ get => HitObject?.X ?? position;
set => position = value;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
index 3e06e78dba..c1b7214d72 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
// We only care about testing misses, hits are tested via JuiceStream
- [TestCase(true)]
+ [TestCase(false)]
public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
index 7c2304694f..d6bba3d55e 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs
@@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Tests
for (int i = 0; i < 100; i++)
{
- float width = (i % 10 + 1) / 20f;
+ float width = (i % 10 + 1) / 20f * CatchPlayfield.WIDTH;
beatmap.HitObjects.Add(new JuiceStream
{
- X = 0.5f - width / 2,
+ X = CatchPlayfield.CENTER_X - width / 2,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
- new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)
+ new Vector2(width, 0)
}),
StartTime = i * 2000,
NewCombo = i % 8 == 0
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
new file mode 100644
index 0000000000..f15da29993
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -0,0 +1,56 @@
+// 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.Allocation;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneCatchModHidden : ModTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false);
+ }
+
+ [Test]
+ public void TestJuiceStream()
+ {
+ CreateModTest(new ModTestData
+ {
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new JuiceStream
+ {
+ StartTime = 1000,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }),
+ X = CatchPlayfield.WIDTH / 2
+ }
+ }
+ },
+ Mod = new CatchModHidden(),
+ PassCondition = () => Player.Results.Count > 0
+ && Player.ChildrenOfType().Single().Alpha > 0
+ && Player.ChildrenOfType().Last().Alpha > 0
+ });
+ }
+
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
index 44672b6526..1ff31697b8 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -22,7 +23,14 @@ namespace osu.Game.Rulesets.Catch.Tests
};
for (int i = 0; i < 512; i++)
- beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 });
+ {
+ beatmap.HitObjects.Add(new Fruit
+ {
+ X = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH,
+ StartTime = i * 100,
+ NewCombo = i % 8 == 0
+ });
+ }
return beatmap;
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index 2b30edb70b..b4f123598b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
@@ -25,6 +26,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{
private RulesetInfo catchRuleset;
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ private Catcher catcher => this.ChildrenOfType().First().MovableCatcher;
+
public TestSceneCatcherArea()
{
AddSliderStep("CircleSize", 0, 8, 5, createCatcher);
@@ -34,24 +40,43 @@ namespace osu.Game.Rulesets.Catch.Tests
AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false)
{
- X = this.ChildrenOfType().First().MovableCatcher.X
+ X = catcher.X
}), 20);
AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
{
- X = this.ChildrenOfType().First().MovableCatcher.X,
+ X = catcher.X,
LastInCombo = true,
}), 20);
AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
{
- X = this.ChildrenOfType().First().MovableCatcher.X,
+ X = catcher.X
}), 20);
AddRepeatStep("miss fruit", () => catchFruit(new Fruit
{
- X = this.ChildrenOfType().First().MovableCatcher.X + 100,
+ X = catcher.X + 100,
LastInCombo = true,
}, true), 20);
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestHitLighting(bool enable)
+ {
+ AddStep("create catcher", () => createCatcher(5));
+
+ AddStep("toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable));
+ AddStep("catch fruit", () => catchFruit(new TestFruit(false)
+ {
+ X = catcher.X
+ }));
+ AddStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
+ {
+ X = catcher.X,
+ LastInCombo = true
+ }));
+ AddAssert("check hit explosion", () => catcher.ChildrenOfType().Any() == enable);
+ }
+
private void catchFruit(Fruit fruit, bool miss = false)
{
this.ChildrenOfType().ForEach(area =>
@@ -76,8 +101,8 @@ namespace osu.Game.Rulesets.Catch.Tests
RelativeSizeAxes = Axes.Both,
Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.TopLeft,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.TopCentre,
CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation
},
});
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
index a7094c00be..d35f828e28 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs
@@ -158,8 +158,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private float getXCoords(bool hit)
{
- const float x_offset = 0.2f;
- float xCoords = drawableRuleset.Playfield.Width / 2;
+ const float x_offset = 0.2f * CatchPlayfield.WIDTH;
+ float xCoords = CatchPlayfield.CENTER_X;
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
index a0dcb86d57..ad24adf352 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs
@@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Tests
};
// Should produce a hyper-dash (edge case test)
- beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56 / 512f, NewCombo = true });
- beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308 / 512f, NewCombo = true });
+ beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true });
+ beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true });
double startTime = 3000;
- const float left_x = 0.02f;
- const float right_x = 0.98f;
+ const float left_x = 0.02f * CatchPlayfield.WIDTH;
+ const float right_x = 0.98f * CatchPlayfield.WIDTH;
createObjects(() => new Fruit { X = left_x });
createObjects(() => new TestJuiceStream(right_x), 1);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
index ffcf61a4bf..269e783899 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
@@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
new JuiceStream
{
- X = 0.5f,
+ X = CatchPlayfield.CENTER_X,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
@@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
},
new Banana
{
- X = 0.5f,
+ X = CatchPlayfield.CENTER_X,
StartTime = 1000,
NewCombo = true
}
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 cbd3dc5518..7c0b73e8c3 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 0de2060e2d..145a40f5f5 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -5,7 +5,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
using System.Linq;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
- X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
+ X = positionData?.X ?? 0,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
@@ -59,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
- X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
+ X = positionData?.X ?? 0
}.Yield();
}
}
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
index 7c81bcdf0c..bb14988414 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case BananaShower bananaShower:
foreach (var banana in bananaShower.NestedHitObjects.OfType())
{
- banana.XOffset = (float)rng.NextDouble();
+ banana.XOffset = (float)(rng.NextDouble() * CatchPlayfield.WIDTH);
rng.Next(); // osu!stable retrieved a random banana type
rng.Next(); // osu!stable retrieved a random banana rotation
rng.Next(); // osu!stable retrieved a random banana colour
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream:
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
- lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X / CatchPlayfield.BASE_WIDTH;
+ lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X;
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
lastStartTime = juiceStream.StartTime;
@@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
catchObject.XOffset = 0;
if (catchObject is TinyDroplet)
- catchObject.XOffset = Math.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X);
+ catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.X, CatchPlayfield.WIDTH - catchObject.X);
else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation
}
@@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
// ReSharper disable once PossibleLossOfFraction
- if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3)
+ if (Math.Abs(positionDiff) < timeDiff / 3)
applyOffset(ref offsetPosition, positionDiff);
hitObject.XOffset = offsetPosition - hitObject.X;
@@ -149,12 +149,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
{
bool right = rng.NextBool();
- float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
+ float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));
if (right)
{
// Clamp to the right bound
- if (position + rand <= 1)
+ if (position + rand <= CatchPlayfield.WIDTH)
position += rand;
else
position -= rand;
@@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
- double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2;
+ double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index ca75a816f1..9437023c70 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -21,11 +21,13 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
+ [ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index 2ee7cea645..d700f79e5b 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (mods.Any(m => m is ModHidden))
{
- value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
// Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0)
value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
index 360af1a8c9..3e21b8fbaf 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs
@@ -3,7 +3,6 @@
using System;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
@@ -33,8 +32,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
- NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
- LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor;
+ NormalizedPosition = BaseObject.X * scalingFactor;
+ LastNormalizedPosition = LastObject.X * scalingFactor;
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(40, DeltaTime);
diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
index 918ed77683..e679231638 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs
@@ -3,7 +3,6 @@
using System;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
@@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
}
// Bonus for edge dashes.
- if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH)
+ if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
{
if (!catchCurrent.LastObject.HyperDash)
edgeDashBonus += 5.7;
@@ -78,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition;
}
- distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
+ distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
}
lastPlayerPosition = playerPosition;
diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
index fc030877f1..a7449ba4e1 100644
--- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
+++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Judgements
return 0;
case HitResult.Perfect:
- return 0.01;
+ return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
index e3391c47f1..fb92399102 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
@@ -1,17 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Rulesets.Catch.Judgements;
-using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModPerfect : ModPerfect
{
- protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
- => !(result.Judgement is CatchBananaJudgement)
- && base.FailCondition(healthProcessor, result);
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs
index 0b3d1d23e0..4ecfb7b16d 100644
--- a/osu.Game.Rulesets.Catch/Objects/Banana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using osu.Game.Audio;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
@@ -8,8 +10,27 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public class Banana : Fruit
{
+ ///
+ /// Index of banana in current shower.
+ ///
+ public int BananaIndex;
+
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement();
+
+ private static readonly List samples = new List { new BananaHitSampleInfo() };
+
+ public Banana()
+ {
+ Samples = samples;
+ }
+
+ private class BananaHitSampleInfo : HitSampleInfo
+ {
+ private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };
+
+ public override IEnumerable LookupNames => lookupNames;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index 04a995c77e..89c51459a6 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -30,15 +30,21 @@ namespace osu.Game.Rulesets.Catch.Objects
if (spacing <= 0)
return;
- for (double i = StartTime; i <= EndTime; i += spacing)
+ double time = StartTime;
+ int i = 0;
+
+ while (time <= EndTime)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new Banana
{
- Samples = Samples,
- StartTime = i
+ StartTime = time,
+ BananaIndex = i,
});
+
+ time += spacing;
+ i++;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index f3b566f340..04932ecdbb 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -5,6 +5,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -17,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Objects
private float x;
+ ///
+ /// The horizontal position of the fruit between 0 and .
+ ///
public float X
{
get => x + XOffset;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
index 01b76ceed9..a865984d45 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -40,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1);
}
+ public override void PlaySamples()
+ {
+ base.PlaySamples();
+ if (Samples != null)
+ Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f;
+ }
+
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
index b12cdd4ccb..c6345a9df7 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Catch.UI;
using osuTK;
using osuTK.Graphics;
@@ -70,12 +71,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
- protected override float SamplePlaybackPosition => HitObject.X;
+ protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject)
{
- RelativePositionAxes = Axes.X;
X = hitObject.X;
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 2c96ee2b19..6b8b70ed54 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -7,7 +7,6 @@ using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -80,7 +79,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
StartTime = t + lastEvent.Value.Time,
X = X + Path.PositionAt(
- lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
+ lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
});
}
}
@@ -97,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = dropletSamples,
StartTime = e.Time,
- X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
+ X = X + Path.PositionAt(e.PathProgress).X,
});
break;
@@ -108,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = Samples,
StartTime = e.Time,
- X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
+ X = X + Path.PositionAt(e.PathProgress).X,
});
break;
}
}
}
- public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
+ public float EndX => X + this.CurvePositionAt(1).X;
public double Duration
{
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 7a33cb0577..5d11c574b1 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Replays
// todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2;
- float lastPosition = 0.5f;
+ float lastPosition = CatchPlayfield.CENTER_X;
double lastTime = 0;
void moveToNext(CatchHitObject h)
@@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Replays
bool impossibleJump = speedRequired > movement_speed * 2;
// todo: get correct catcher size, based on difficulty CS.
- const float catcher_width_half = CatcherArea.CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * 0.3f * 0.5f;
+ const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f;
if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X)
{
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
index f122588a2b..99d899db80 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
@@ -35,18 +35,15 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- if (!Position.HasValue) return new List();
+ if (!Position.HasValue) return;
- return new List
+ inputs.Add(new CatchReplayState
{
- new CatchReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List(),
- CatcherX = Position.Value
- },
- };
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ CatcherX = Position.Value
+ });
}
public class CatchReplayState : ReplayState
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
index 9dab3ed630..7efd832f62 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs
@@ -4,7 +4,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
-using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
- Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH;
+ Position = currentFrame.Position.X;
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
if (Dashing)
@@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Replays
if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1;
- return new LegacyReplayFrame(Time, Position * CatchPlayfield.BASE_WIDTH, null, state);
+ return new LegacyReplayFrame(Time, Position, null, state);
}
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 2319c5ac1f..154e1576db 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatchPlayfield : ScrollingPlayfield
{
- public const float BASE_WIDTH = 512;
+ ///
+ /// The width of the playfield.
+ /// The horizontal movement of the catcher is confined in the area of this width.
+ ///
+ public const float WIDTH = 512;
+
+ ///
+ /// The center position of the playfield.
+ ///
+ public const float CENTER_X = WIDTH / 2;
internal readonly CatcherArea CatcherArea;
@@ -26,22 +35,25 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation)
{
- Container explodingFruitContainer;
-
- InternalChildren = new Drawable[]
+ var explodingFruitContainer = new Container
{
- explodingFruitContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- },
- CatcherArea = new CatcherArea(difficulty)
- {
- CreateDrawableRepresentation = createDrawableRepresentation,
- ExplodingFruitTarget = explodingFruitContainer,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.TopLeft,
- },
- HitObjectContainer
+ RelativeSizeAxes = Axes.Both,
+ };
+
+ CatcherArea = new CatcherArea(difficulty)
+ {
+ CreateDrawableRepresentation = createDrawableRepresentation,
+ ExplodingFruitTarget = explodingFruitContainer,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.TopLeft,
+ };
+
+ InternalChildren = new[]
+ {
+ explodingFruitContainer,
+ CatcherArea.MovableCatcher.CreateProxiedContent(),
+ HitObjectContainer,
+ CatcherArea
};
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
index b8d3dc9017..8ee23461ba 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
- Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH);
+ Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
Size = Vector2.Divide(Vector2.One, Scale);
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 9cce46d730..8820dff730 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -5,12 +5,14 @@ using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
@@ -42,10 +44,16 @@ namespace osu.Game.Rulesets.Catch.UI
///
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
///
- public const double BASE_SPEED = 1.0 / 512;
+ public const double BASE_SPEED = 1.0;
public Container ExplodingFruitTarget;
+ private Container caughtFruitContainer { get; } = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
+ };
+
[NotNull]
private readonly Container trailsTarget;
@@ -83,8 +91,6 @@ namespace osu.Game.Rulesets.Catch.UI
///
private readonly float catchWidth;
- private Container caughtFruit;
-
private CatcherSprite catcherIdle;
private CatcherSprite catcherKiai;
private CatcherSprite catcherFail;
@@ -99,14 +105,12 @@ namespace osu.Game.Rulesets.Catch.UI
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
+ private Bindable hitLighting;
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
{
this.trailsTarget = trailsTarget;
- RelativePositionAxes = Axes.X;
- X = 0.5f;
-
Origin = Anchor.TopCentre;
Size = new Vector2(CatcherArea.CATCHER_SIZE);
@@ -117,15 +121,13 @@ namespace osu.Game.Rulesets.Catch.UI
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
+ hitLighting = config.GetBindable(OsuSetting.HitLighting);
+
InternalChildren = new Drawable[]
{
- caughtFruit = new Container
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.BottomCentre,
- },
+ caughtFruitContainer,
catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
{
Anchor = Anchor.TopCentre,
@@ -148,6 +150,11 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher();
}
+ ///
+ /// Creates proxied content to be displayed beneath hitobjects.
+ ///
+ public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy();
+
///
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
///
@@ -179,7 +186,7 @@ namespace osu.Game.Rulesets.Catch.UI
const float allowance = 10;
- while (caughtFruit.Any(f =>
+ while (caughtFruitContainer.Any(f =>
f.LifetimeEnd == double.MaxValue &&
Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
{
@@ -190,13 +197,16 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
- caughtFruit.Add(fruit);
+ caughtFruitContainer.Add(fruit);
- AddInternal(new HitExplosion(fruit)
+ if (hitLighting.Value)
{
- X = fruit.X,
- Scale = new Vector2(fruit.HitObject.Scale)
- });
+ AddInternal(new HitExplosion(fruit)
+ {
+ X = fruit.X,
+ Scale = new Vector2(fruit.HitObject.Scale)
+ });
+ }
}
///
@@ -209,8 +219,8 @@ namespace osu.Game.Rulesets.Catch.UI
var halfCatchWidth = catchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
- var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH;
- var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH;
+ var catchObjectPosition = fruit.X;
+ var catcherPosition = Position.X;
var validCatch =
catchObjectPosition >= catcherPosition - halfCatchWidth &&
@@ -224,7 +234,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
var target = fruit.HyperDashTarget;
var timeDifference = target.StartTime - fruit.StartTime;
- double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition;
+ double positionDifference = target.X - catcherPosition;
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
SetHyperDashState(Math.Abs(velocity), target.X);
@@ -331,7 +341,7 @@ namespace osu.Game.Rulesets.Catch.UI
public void UpdatePosition(float position)
{
- position = Math.Clamp(position, 0, 1);
+ position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
if (position == X)
return;
@@ -345,7 +355,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
public void Drop()
{
- foreach (var f in caughtFruit.ToArray())
+ foreach (var f in caughtFruitContainer.ToArray())
Drop(f);
}
@@ -354,7 +364,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
public void Explode()
{
- foreach (var f in caughtFruit.ToArray())
+ foreach (var f in caughtFruitContainer.ToArray())
Explode(f);
}
@@ -453,9 +463,9 @@ namespace osu.Game.Rulesets.Catch.UI
if (ExplodingFruitTarget != null)
{
fruit.Anchor = Anchor.TopLeft;
- fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
+ fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
- if (!caughtFruit.Remove(fruit))
+ if (!caughtFruitContainer.Remove(fruit))
// we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
// this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
return;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 37d177b936..4255c3b1af 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.UI
public Func> CreateDrawableRepresentation;
+ public readonly Catcher MovableCatcher;
+
public Container ExplodingFruitTarget
{
set => MovableCatcher.ExplodingFruitTarget = value;
@@ -31,14 +33,8 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherArea(BeatmapDifficulty difficulty = null)
{
- RelativeSizeAxes = Axes.X;
- Height = CATCHER_SIZE;
- Child = MovableCatcher = new Catcher(this, difficulty);
- }
-
- public static float GetCatcherSize(BeatmapDifficulty difficulty)
- {
- return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
+ Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
+ Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
}
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)
@@ -110,7 +106,5 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
}
-
- protected internal readonly Catcher MovableCatcher;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs
index d8f87195d1..c8feb4ae24 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapSampleConversionTest.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase("convert-samples")]
[TestCase("mania-samples")]
+ [TestCase("slider-convert-samples")]
public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
@@ -29,13 +30,16 @@ namespace osu.Game.Rulesets.Mania.Tests
StartTime = hitObject.StartTime,
EndTime = hitObject.GetEndTime(),
Column = ((ManiaHitObject)hitObject).Column,
- NodeSamples = getSampleNames((hitObject as HoldNote)?.NodeSamples)
+ Samples = getSampleNames(hitObject.Samples),
+ NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples)
};
}
- private IList> getSampleNames(List> hitSampleInfo)
- => hitSampleInfo?.Select(samples =>
- (IList)samples.Select(sample => sample.LookupNames.First()).ToList())
+ private IList getSampleNames(IList hitSampleInfo)
+ => hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList();
+
+ private IList> getNodeSampleNames(List> hitSampleInfo)
+ => hitSampleInfo?.Select(getSampleNames)
.ToList();
protected override Ruleset CreateRuleset() => new ManiaRuleset();
@@ -51,14 +55,19 @@ namespace osu.Game.Rulesets.Mania.Tests
public double StartTime;
public double EndTime;
public int Column;
+ public IList Samples;
public IList> NodeSamples;
public bool Equals(SampleConvertValue other)
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
- && samplesEqual(NodeSamples, other.NodeSamples);
+ && samplesEqual(Samples, other.Samples)
+ && nodeSamplesEqual(NodeSamples, other.NodeSamples);
- private static bool samplesEqual(ICollection> firstSampleList, ICollection> secondSampleList)
+ private static bool samplesEqual(ICollection firstSampleList, ICollection secondSampleList)
+ => firstSampleList.SequenceEqual(secondSampleList);
+
+ private static bool nodeSamplesEqual(ICollection> firstSampleList, ICollection> secondSampleList)
{
if (firstSampleList == null && secondSampleList == null)
return true;
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png
similarity index 100%
rename from osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png
rename to osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-left.png
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png
similarity index 100%
rename from osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png
rename to osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania/stage-right.png
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
index 941abac1da..36765d61bf 100644
--- a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
+++ b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/skin.ini
@@ -9,4 +9,6 @@ Hit50: mania/hit50
Hit100: mania/hit100
Hit200: mania/hit200
Hit300: mania/hit300
-Hit300g: mania/hit300g
\ No newline at end of file
+Hit300g: mania/hit300g
+StageLeft: mania/stage-left
+StageRight: mania/stage-right
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
index a692c0b697..0c56f7bcf4 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
@@ -1,23 +1,27 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Skinning;
+using osu.Game.Rulesets.Objects;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
[TestFixture]
public class TestSceneHitExplosion : ManiaSkinnableTestScene
{
+ private readonly List> hitExplosionPools = new List>();
+
public TestSceneHitExplosion()
{
int runcount = 0;
@@ -29,28 +33,40 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (runcount % 15 > 12)
return;
- CreatedDrawables.OfType().ForEach(c =>
+ int poolIndex = 0;
+
+ foreach (var c in CreatedDrawables.OfType())
{
- c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, 0),
- _ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }));
- });
+ c.Add(hitExplosionPools[poolIndex].Get(e =>
+ {
+ e.Apply(new JudgementResult(new HitObject(), runcount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
+
+ e.Anchor = Anchor.Centre;
+ e.Origin = Anchor.Centre;
+ }));
+
+ poolIndex++;
+ }
}, 100);
}
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new ColumnTestContainer(0, ManiaAction.Key1)
+ SetContents(() =>
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativePositionAxes = Axes.Y,
- Y = -0.25f,
- Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
+ var pool = new DrawablePool(5);
+ hitExplosionPools.Add(pool);
+
+ return new ColumnTestContainer(0, ManiaAction.Key1)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.Y,
+ Y = -0.25f,
+ Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
+ Child = pool
+ };
});
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 0d13b85901..95072cf4f8 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -2,6 +2,7 @@
// 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.Screens;
using osu.Game.Beatmaps;
@@ -10,6 +11,8 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -236,6 +239,53 @@ namespace osu.Game.Rulesets.Mania.Tests
assertTailJudgement(HitResult.Meh);
}
+ [Test]
+ public void TestMissReleaseAndHitSecondRelease()
+ {
+ var windows = new ManiaHitWindows();
+ windows.SetDifficulty(10);
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ Duration = 500,
+ Column = 0,
+ },
+ new HoldNote
+ {
+ StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10,
+ Duration = 500,
+ Column = 0,
+ },
+ },
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ SliderTickRate = 4,
+ OverallDifficulty = 10,
+ },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1),
+ new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()),
+ }, beatmap);
+
+ AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
+ .All(j => j.Type == HitResult.Miss));
+
+ AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
+ .All(j => j.Type == HitResult.Perfect));
+ }
+
private void assertHeadJudgement(HitResult result)
=> AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
@@ -250,11 +300,11 @@ namespace osu.Game.Rulesets.Mania.Tests
private ScoreAccessibleReplayPlayer currentPlayer;
- private void performTest(List frames)
+ private void performTest(List frames, Beatmap beatmap = null)
{
- AddStep("load player", () =>
+ if (beatmap == null)
{
- Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ beatmap = new Beatmap
{
HitObjects =
{
@@ -270,9 +320,14 @@ namespace osu.Game.Rulesets.Mania.Tests
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
- });
+ };
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ }
+
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
new file mode 100644
index 0000000000..8698ba3abd
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestScenePlayfieldCoveringContainer : OsuTestScene
+ {
+ private readonly ScrollingTestContainer scrollingContainer;
+ private readonly PlayfieldCoveringWrapper cover;
+
+ public TestScenePlayfieldCoveringContainer()
+ {
+ Child = scrollingContainer = new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(300, 500),
+ Child = cover = new PlayfieldCoveringWrapper(new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Orange
+ })
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ };
+ }
+
+ [Test]
+ public void TestScrollingDownwards()
+ {
+ AddStep("set down scroll", () => scrollingContainer.Direction = ScrollingDirection.Down);
+ AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
+ AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
+ AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
+ }
+
+ [Test]
+ public void TestScrollingUpwards()
+ {
+ AddStep("set up scroll", () => scrollingContainer.Direction = ScrollingDirection.Up);
+ AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
+ AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
+ AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
+ }
+ }
+}
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 77c871718b..972cbec4a2 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 9fbdf58e21..d03eb0b3c9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -483,9 +483,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (!(HitObject is IHasPathWithRepeats curveData))
return null;
- double segmentTime = (EndTime - HitObject.StartTime) / spanCount;
-
- int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
+ // mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
+ var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
// avoid slicing the list & creating copies, if at all possible.
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
index 53db676a54..53967ffa05 100644
--- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Judgements
return 300;
case HitResult.Perfect:
- return 320;
+ return 350;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 6ddb052585..68dce8b139 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -12,6 +12,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -30,9 +31,11 @@ using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Scoring;
+using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania
{
+ [ExcludeFromDynamicCompile]
public class ManiaRuleset : Ruleset, ILegacyRuleset
{
///
@@ -309,6 +312,21 @@ namespace osu.Game.Rulesets.Mania
{
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v);
}
+
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ {
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ }
+ };
}
public enum PlayfieldType
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index 4c125ad6ef..cbdcd49c5b 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
@@ -1,23 +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 System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModFadeIn : Mod
+ public class ManiaModFadeIn : ManiaModHidden
{
public override string Name => "Fade In";
public override string Acronym => "FI";
public override IconUsage? Icon => OsuIcon.ModHidden;
- public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Keys appear out of nowhere!";
- public override double ScoreMultiplier => 1;
- public override bool Ranked => true;
- public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
+
+ protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
index 66b90984b4..4bdb15526f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
@@ -2,15 +2,44 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModHidden : ModHidden
+ public class ManiaModHidden : ModHidden, IApplicableToDrawableRuleset
{
public override string Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
+
+ ///
+ /// The direction in which the cover should expand.
+ ///
+ protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
+
+ public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
+
+ foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
+ {
+ HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
+ Container hocParent = (Container)hoc.Parent;
+
+ hocParent.Remove(hoc);
+ hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
+ {
+ c.RelativeSizeAxes = Axes.Both;
+ c.Direction = ExpandDirection;
+ c.Coverage = 0.5f;
+ }));
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 2262bd2b7d..0c5289efe1 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -167,6 +167,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
+ // The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
+ // But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
+ // Note: Unlike below, we use the tail's start time to determine the time offset.
+ if (Time.Current > Tail.HitObject.StartTime && !Tail.HitObject.HitWindows.CanBeHit(Time.Current - Tail.HitObject.StartTime))
+ return false;
+
beginHoldAt(Time.Current - Head.HitObject.StartTime);
Head.UpdateResult();
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs
index 899718b77e..aa0c148caf 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Mania.Replays
protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any();
- public override List GetPendingInputs() => new List { new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() } };
+ public override void CollectPendingInputs(List inputs)
+ {
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
index b8ce85eef5..fec1360b26 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples-expected-conversion.json
@@ -9,7 +9,8 @@
["normal-hitnormal"],
["soft-hitnormal"],
["drum-hitnormal"]
- ]
+ ],
+ "Samples": ["drum-hitnormal"]
}, {
"StartTime": 1875.0,
"EndTime": 2750.0,
@@ -17,14 +18,16 @@
"NodeSamples": [
["soft-hitnormal"],
["drum-hitnormal"]
- ]
+ ],
+ "Samples": ["drum-hitnormal"]
}]
}, {
"StartTime": 3750.0,
"Objects": [{
"StartTime": 3750.0,
"EndTime": 3750.0,
- "Column": 3
+ "Column": 3,
+ "Samples": ["normal-hitnormal"]
}]
}]
}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu
index 16b73992d2..fea1de6614 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/convert-samples.osu
@@ -13,4 +13,4 @@ SliderTickRate:1
[HitObjects]
88,99,1000,6,0,L|306:259,2,245,0|0|0,1:0|2:0|3:0,0:0:0:0:
-259,118,3750,1,0,0:0:0:0:
+259,118,3750,1,0,1:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json
index e22540614d..1aca75a796 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples-expected-conversion.json
@@ -8,7 +8,8 @@
"NodeSamples": [
["normal-hitnormal"],
[]
- ]
+ ],
+ "Samples": ["normal-hitnormal"]
}]
}, {
"StartTime": 2000.0,
@@ -19,7 +20,8 @@
"NodeSamples": [
["drum-hitnormal"],
[]
- ]
+ ],
+ "Samples": ["drum-hitnormal"]
}]
}]
}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json
new file mode 100644
index 0000000000..e3768a90d7
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples-expected-conversion.json
@@ -0,0 +1,21 @@
+{
+ "Mappings": [{
+ "StartTime": 8470.0,
+ "Objects": [{
+ "StartTime": 8470.0,
+ "EndTime": 8470.0,
+ "Column": 0,
+ "Samples": ["normal-hitnormal", "normal-hitclap"]
+ }, {
+ "StartTime": 8626.470587768974,
+ "EndTime": 8626.470587768974,
+ "Column": 1,
+ "Samples": ["normal-hitnormal"]
+ }, {
+ "StartTime": 8782.941175537948,
+ "EndTime": 8782.941175537948,
+ "Column": 2,
+ "Samples": ["normal-hitnormal", "normal-hitclap"]
+ }]
+ }]
+}
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu
new file mode 100644
index 0000000000..08e90ce807
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/slider-convert-samples.osu
@@ -0,0 +1,15 @@
+osu file format v14
+
+[Difficulty]
+HPDrainRate:6
+CircleSize:4
+OverallDifficulty:8
+ApproachRate:9.5
+SliderMultiplier:2.00000000596047
+SliderTickRate:1
+
+[TimingPoints]
+0,312.941176470588,4,1,0,100,1,0
+
+[HitObjects]
+82,216,8470,6,0,P|52:161|99:113,2,100,8|0|8,1:0|1:0|1:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 9b54b48de3..4b2f643333 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor
{
+ protected override double DefaultAccuracyPortion => 0.95;
+
+ protected override double DefaultComboPortion => 0.05;
+
public override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
index a749f80855..9f716428c0 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
@@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
- sprite = skin.GetAnimation(imageName, true, true).With(d =>
+ sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
{
if (d == null)
return;
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
index bc93bb2615..12747924de 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
@@ -6,13 +6,15 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning
{
- public class LegacyHitExplosion : LegacyManiaColumnElement
+ public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion
{
private readonly IBindable direction = new Bindable();
@@ -62,9 +64,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
explosion.Anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}
- protected override void LoadComplete()
+ public void Animate(JudgementResult result)
{
- base.LoadComplete();
+ (explosion as IFramedAnimation)?.GotoFrame(0);
explosion?.FadeInFromZero(80)
.Then().FadeOut(120);
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
index 515c941d65..283b04373b 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.UI.Scrolling;
@@ -92,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
string noteImage = GetColumnSkinConfig(skin, lookup)?.Value
?? $"mania-note{FallbackColumnIndex}{suffix}";
- return skin.GetTexture(noteImage);
+ return skin.GetTexture(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 511d6c8623..255ce4c064 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -9,9 +9,9 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable Action = new Bindable();
- private readonly ColumnHitObjectArea hitObjectArea;
-
+ public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container TopLevelContainer;
+ private readonly DrawablePool hitExplosionPool;
- public Container UnderlayElements => hitObjectArea.UnderlayElements;
+ public Container UnderlayElements => HitObjectArea.UnderlayElements;
public Column(int index)
{
@@ -53,9 +53,10 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[]
{
+ hitExplosionPool = new DrawablePool(5),
// 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(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
+ HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
@@ -64,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
- TopLevelContainer.Add(hitObjectArea.Explosions.CreateProxy());
+ TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
}
public override Axes RelativeSizeAxes => Axes.Y;
@@ -108,15 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ =>
- new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick))
- {
- RelativeSizeAxes = Axes.Both
- };
-
- hitObjectArea.Explosions.Add(explosion);
-
- explosion.Delay(200).Expire(true);
+ HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
public bool OnPressed(ManiaAction action)
diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
index ba5281a1a2..8f7880dafa 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -14,12 +15,14 @@ namespace osu.Game.Rulesets.Mania.UI.Components
public class HitObjectArea : SkinReloadableDrawable
{
protected readonly IBindable Direction = new Bindable();
+ public readonly HitObjectContainer HitObjectContainer;
public HitObjectArea(HitObjectContainer hitObjectContainer)
{
- InternalChildren = new[]
+ InternalChild = new Container
{
- hitObjectContainer,
+ RelativeSizeAxes = Axes.Both,
+ Child = HitObjectContainer = hitObjectContainer
};
}
diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
index 7a047ed121..225269cf48 100644
--- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -8,6 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -15,35 +17,36 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
- public class DefaultHitExplosion : CompositeDrawable
+ public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
{
+ private const float default_large_faint_size = 0.8f;
+
public override bool RemoveWhenNotAlive => true;
+ [Resolved]
+ private Column column { get; set; }
+
private readonly IBindable direction = new Bindable();
- private readonly CircularContainer largeFaint;
- private readonly CircularContainer mainGlow1;
+ private CircularContainer largeFaint;
+ private CircularContainer mainGlow1;
- public DefaultHitExplosion(Color4 objectColour, bool isSmall = false)
+ public DefaultHitExplosion()
{
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.X;
Height = DefaultNotePiece.NOTE_HEIGHT;
+ }
- // scale roughly in-line with visual appearance of notes
- Scale = new Vector2(1f, 0.6f);
-
- if (isSmall)
- Scale *= 0.5f;
-
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
const float angle_variangle = 15; // should be less than 45
-
const float roundness = 80;
-
const float initial_height = 10;
- var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
+ var colour = Interpolation.ValueAt(0.4f, column.AccentColour, Color4.White, 0, 1);
InternalChildren = new Drawable[]
{
@@ -54,12 +57,12 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both,
Masking = true,
// we want our size to be very small so the glow dominates it.
- Size = new Vector2(0.8f),
+ Size = new Vector2(default_large_faint_size),
Blending = BlendingParameters.Additive,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+ Colour = Interpolation.ValueAt(0.1f, column.AccentColour, Color4.White, 0, 1).Opacity(0.3f),
Roundness = 160,
Radius = 200,
},
@@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.UI
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+ Colour = Interpolation.ValueAt(0.6f, column.AccentColour, Color4.White, 0, 1),
Roundness = 20,
Radius = 50,
},
@@ -114,30 +117,11 @@ namespace osu.Game.Rulesets.Mania.UI
},
}
};
- }
- [BackgroundDependencyLoader]
- private void load(IScrollingInfo scrollingInfo)
- {
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
}
- protected override void LoadComplete()
- {
- const double duration = 200;
-
- base.LoadComplete();
-
- largeFaint
- .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
- .FadeOut(duration * 2);
-
- mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint);
-
- this.FadeOut(duration, Easing.Out);
- }
-
private void onDirectionChanged(ValueChangedEvent direction)
{
if (direction.NewValue == ScrollingDirection.Up)
@@ -151,5 +135,29 @@ namespace osu.Game.Rulesets.Mania.UI
Y = -DefaultNotePiece.NOTE_HEIGHT / 2;
}
}
+
+ public void Animate(JudgementResult result)
+ {
+ // scale roughly in-line with visual appearance of notes
+ Vector2 scale = new Vector2(1, 0.6f);
+
+ if (result.Judgement is HoldNoteTickJudgement)
+ scale *= 0.5f;
+
+ this.ScaleTo(scale);
+
+ largeFaint
+ .ResizeTo(default_large_faint_size)
+ .Then()
+ .ResizeTo(default_large_faint_size * new Vector2(5, 1), PoolableHitExplosion.DURATION, Easing.OutQuint)
+ .FadeOut(PoolableHitExplosion.DURATION * 2);
+
+ mainGlow1
+ .ScaleTo(1)
+ .Then()
+ .ScaleTo(1.4f, PoolableHitExplosion.DURATION, Easing.OutQuint);
+
+ this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index 8797f014df..d99f6cb8d3 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -15,6 +15,10 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
+ public DrawableManiaJudgement()
+ {
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs
new file mode 100644
index 0000000000..3252dcc276
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/IHitExplosion.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.Game.Rulesets.Judgements;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// Common interface for all hit explosion bodies.
+ ///
+ public interface IHitExplosion
+ {
+ ///
+ /// Begins animating this .
+ ///
+ /// The type of that caused this explosion.
+ void Animate(JudgementResult result);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs
new file mode 100644
index 0000000000..15d216e8c5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs
@@ -0,0 +1,133 @@
+// 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.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// A that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield.
+ ///
+ public class PlayfieldCoveringWrapper : CompositeDrawable
+ {
+ ///
+ /// The complete cover, including gradient and fill.
+ ///
+ private readonly Drawable cover;
+
+ ///
+ /// The gradient portion of the cover.
+ ///
+ private readonly Box gradient;
+
+ ///
+ /// The fully-opaque portion of the cover.
+ ///
+ private readonly Box filled;
+
+ private readonly IBindable scrollDirection = new Bindable();
+
+ public PlayfieldCoveringWrapper(Drawable content)
+ {
+ InternalChild = new BufferedContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ content,
+ cover = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Blending = new BlendingParameters
+ {
+ // Don't change the destination colour.
+ RGBEquation = BlendingEquation.Add,
+ Source = BlendingType.Zero,
+ Destination = BlendingType.One,
+ // Subtract the cover's alpha from the destination (points with alpha 1 should make the destination completely transparent).
+ AlphaEquation = BlendingEquation.Add,
+ SourceAlpha = BlendingType.Zero,
+ DestinationAlpha = BlendingType.OneMinusSrcAlpha
+ },
+ Children = new Drawable[]
+ {
+ gradient = new Box
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Both,
+ Height = 0.25f,
+ Colour = ColourInfo.GradientVertical(
+ Color4.White.Opacity(0f),
+ Color4.White.Opacity(1f)
+ )
+ },
+ filled = new Box
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0
+ }
+ }
+ }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ scrollDirection.BindTo(scrollingInfo.Direction);
+ scrollDirection.BindValueChanged(onScrollDirectionChanged, true);
+ }
+
+ private void onScrollDirectionChanged(ValueChangedEvent direction)
+ => cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f;
+
+ ///
+ /// The relative area that should be completely covered. This does not include the fade.
+ ///
+ public float Coverage
+ {
+ set
+ {
+ filled.Height = value;
+ gradient.Y = -value;
+ }
+ }
+
+ ///
+ /// The direction in which the cover expands.
+ ///
+ public CoverExpandDirection Direction
+ {
+ set => cover.Scale = value == CoverExpandDirection.AlongScroll ? Vector2.One : new Vector2(1, -1);
+ }
+ }
+
+ public enum CoverExpandDirection
+ {
+ ///
+ /// The cover expands along the scrolling direction.
+ ///
+ AlongScroll,
+
+ ///
+ /// The cover expands against the scrolling direction.
+ ///
+ AgainstScroll
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
new file mode 100644
index 0000000000..64b7d7d550
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ public class PoolableHitExplosion : PoolableDrawable
+ {
+ public const double DURATION = 200;
+
+ public JudgementResult Result { get; private set; }
+
+ [Resolved]
+ private Column column { get; set; }
+
+ private SkinnableDrawable skinnableExplosion;
+
+ public PoolableHitExplosion()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion())
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+
+ public void Apply(JudgementResult result)
+ {
+ Result = result;
+ }
+
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
+ (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 faa04dea97..36780b0f80 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -33,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI
public IReadOnlyList Columns => columnFlow.Children;
private readonly FillFlowContainer columnFlow;
- public Container Judgements => judgements;
private readonly JudgementContainer judgements;
+ private readonly DrawablePool judgementPool;
private readonly Drawable barLineContainer;
private readonly Container topLevelContainer;
@@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new Drawable[]
{
+ judgementPool = new DrawablePool(2),
new Container
{
Anchor = Anchor.TopCentre,
@@ -208,12 +210,14 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- judgements.Clear();
- judgements.Add(new DrawableManiaJudgement(result, judgedObject)
+ judgements.Clear(false);
+ judgements.Add(judgementPool.Get(j =>
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- });
+ j.Apply(result, judgedObject);
+
+ j.Anchor = Anchor.Centre;
+ j.Origin = Anchor.Centre;
+ }));
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
index a0a38fc47b..cad98185ce 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
@@ -1,12 +1,27 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public abstract class OsuSkinnableTestScene : SkinnableTestScene
{
+ private Container content;
+
+ protected override Container Content
+ {
+ get
+ {
+ if (content == null)
+ base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
+
+ return content;
+ }
+ }
+
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png
new file mode 100644
index 0000000000..3811e5050f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png
new file mode 100644
index 0000000000..d84eab2f15
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png
new file mode 100644
index 0000000000..4dd4a6d319
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png
new file mode 100644
index 0000000000..c66f1c9309
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png
new file mode 100644
index 0000000000..33902186d9
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png
new file mode 100644
index 0000000000..6882a232e0
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png
new file mode 100644
index 0000000000..98a9991c2f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav
new file mode 100644
index 0000000000..5e583e77aa
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav
new file mode 100644
index 0000000000..bba19381f1
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav differ
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
new file mode 100644
index 0000000000..10d9d7ffde
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs
@@ -0,0 +1,132 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.Threading;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu.Statistics;
+using osu.Game.Scoring;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene
+ {
+ private Box background;
+ private Drawable object1;
+ private Drawable object2;
+ private TestAccuracyHeatmap accuracyHeatmap;
+ private ScheduledDelegate automaticAdditionDelegate;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ automaticAdditionDelegate?.Cancel();
+ automaticAdditionDelegate = null;
+
+ Children = new[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex("#333"),
+ },
+ object1 = new BorderCircle
+ {
+ Position = new Vector2(256, 192),
+ Colour = Color4.Yellow,
+ },
+ object2 = new BorderCircle
+ {
+ Position = new Vector2(100, 300),
+ },
+ accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo })
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(130)
+ }
+ };
+ });
+
+ [Test]
+ public void TestManyHitPointsAutomatic()
+ {
+ AddStep("add scheduled delegate", () =>
+ {
+ automaticAdditionDelegate = Scheduler.AddDelayed(() =>
+ {
+ var randomPos = new Vector2(
+ RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2),
+ RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2));
+
+ // The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene).
+ accuracyHeatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500));
+ InputManager.MoveMouseTo(background.ToScreenSpace(randomPos));
+ }, 1, true);
+ });
+
+ AddWaitStep("wait for some hit points", 10);
+ }
+
+ [Test]
+ public void TestManualPlacement()
+ {
+ AddStep("return user input", () => InputManager.UseParentInput = true);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50);
+ return true;
+ }
+
+ private class TestAccuracyHeatmap : AccuracyHeatmap
+ {
+ public TestAccuracyHeatmap(ScoreInfo score)
+ : base(score, new TestBeatmap(new OsuRuleset().RulesetInfo))
+ {
+ }
+
+ public new void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
+ => base.AddPoint(start, end, hitPoint, radius);
+ }
+
+ private class BorderCircle : CircularContainer
+ {
+ public BorderCircle()
+ {
+ Origin = Anchor.Centre;
+ Size = new Vector2(100);
+
+ Masking = true;
+ BorderThickness = 2;
+ BorderColour = Color4.White;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ },
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(4),
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index 46769f65fe..dde02e873b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing.Input;
using osu.Game.Audio;
@@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
- public Texture GetTexture(string componentName)
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
switch (componentName)
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index c81edf4e07..646f12f710 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -2,29 +2,111 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Collections.Generic;
using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Framework.Testing;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneDrawableJudgement : OsuSkinnableTestScene
{
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ private readonly List> pools;
+
public TestSceneDrawableJudgement()
{
+ pools = new List>();
+
foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1))
+ showResult(result);
+ }
+
+ [Test]
+ public void TestHitLightingDisabled()
+ {
+ AddStep("hit lighting disabled", () => config.Set(OsuSetting.HitLighting, false));
+
+ showResult(HitResult.Great);
+
+ AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
+ AddAssert("judgement body immediately visible",
+ () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha == 1));
+ AddAssert("hit lighting hidden",
+ () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0));
+ }
+
+ [Test]
+ public void TestHitLightingEnabled()
+ {
+ AddStep("hit lighting enabled", () => config.Set(OsuSetting.HitLighting, true));
+
+ showResult(HitResult.Great);
+
+ AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
+ AddAssert("judgement body not immediately visible",
+ () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
+ AddAssert("hit lighting shown",
+ () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0));
+ }
+
+ private void showResult(HitResult result)
+ {
+ AddStep("Show " + result.GetDescription(), () =>
{
- AddStep("Show " + result.GetDescription(), () => SetContents(() =>
- new DrawableOsuJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)
+ int poolIndex = 0;
+
+ SetContents(() =>
+ {
+ DrawablePool pool;
+
+ if (poolIndex >= pools.Count)
+ pools.Add(pool = new DrawablePool(1));
+ else
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }));
- }
+ pool = pools[poolIndex];
+
+ // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent.
+ ((Container)pool.Parent).Clear(false);
+ }
+
+ var container = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ pool,
+ pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
+ {
+ j.Anchor = Anchor.Centre;
+ j.Origin = Anchor.Centre;
+ })
+ }
+ };
+
+ poolIndex++;
+ return container;
+ });
+ });
+ }
+
+ private class TestDrawableOsuJudgement : DrawableOsuJudgement
+ {
+ public new SkinnableSprite Lighting => base.Lighting;
+ public new Container JudgementBody => base.JudgementBody;
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 38c2bb9b95..461779b185 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -5,7 +5,9 @@ using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing.Input;
+using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
@@ -24,9 +26,34 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved]
private OsuConfigManager config { get; set; }
+ private Drawable background;
+
public TestSceneGameplayCursor()
{
gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
+
+ AddStep("change background colour", () =>
+ {
+ background?.Expire();
+
+ Add(background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = float.MaxValue,
+ Colour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)
+ });
+ });
+
+ AddSliderStep("circle size", 0f, 10f, 0f, val =>
+ {
+ config.Set(OsuSetting.AutoCursorSize, true);
+ gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val;
+ Scheduler.AddOnce(recreate);
+ });
+
+ AddStep("test cursor container", recreate);
+
+ void recreate() => SetContents(() => new OsuInputManager(new OsuRuleset().RulesetInfo) { Child = new OsuCursorContainer() });
}
[TestCase(1, 1)]
@@ -69,16 +96,27 @@ namespace osu.Game.Rulesets.Osu.Tests
private class ClickingCursorContainer : OsuCursorContainer
{
+ private bool pressed;
+
+ public bool Pressed
+ {
+ set
+ {
+ if (value == pressed)
+ return;
+
+ pressed = value;
+ if (value)
+ OnPressed(OsuAction.LeftButton);
+ else
+ OnReleased(OsuAction.LeftButton);
+ }
+ }
+
protected override void Update()
{
base.Update();
-
- double currentTime = Time.Current;
-
- if (((int)(currentTime / 1000)) % 2 == 0)
- OnPressed(OsuAction.LeftButton);
- else
- OnReleased(OsuAction.LeftButton);
+ Pressed = ((int)(Time.Current / 1000)) % 2 == 0;
}
}
@@ -87,6 +125,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public MovingCursorInputManager()
{
UseParentInput = false;
+ ShowVisualCursorGuide = false;
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
index c3b4d2625e..854626d362 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
const double time_slider = 1500;
const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero;
- Vector2 positionSlider = new Vector2(80);
+ Vector2 positionSlider = new Vector2(30);
var hitObjects = new List
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index b357e20ee8..075bf314bc 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -9,6 +9,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Timing;
@@ -131,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests
};
}
- public Texture GetTexture(string componentName) => null;
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public SampleChannel GetSample(ISampleInfo sampleInfo) => null;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index a9404f665a..6a689a1f80 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -26,19 +25,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture]
public class TestSceneSlider : OsuSkinnableTestScene
{
- private Container content;
-
- protected override Container Content
- {
- get
- {
- if (content == null)
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
-
- return content;
- }
- }
-
private int depthIndex;
public TestSceneSlider()
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index 65bed071cd..47b3926ceb 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -4,57 +4,76 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneSpinner : OsuTestScene
+ public class TestSceneSpinner : OsuSkinnableTestScene
{
- private readonly Container content;
- protected override Container Content => content;
-
private int depthIndex;
- public TestSceneSpinner()
- {
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
+ private TestDrawableSpinner drawableSpinner;
- AddStep("Miss Big", () => testSingle(2));
- AddStep("Miss Medium", () => testSingle(5));
- AddStep("Miss Small", () => testSingle(7));
- AddStep("Hit Big", () => testSingle(2, true));
- AddStep("Hit Medium", () => testSingle(5, true));
- AddStep("Hit Small", () => testSingle(7, true));
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestVariousSpinners(bool autoplay)
+ {
+ string term = autoplay ? "Hit" : "Miss";
+ AddStep($"{term} Big", () => SetContents(() => testSingle(2, autoplay)));
+ AddStep($"{term} Medium", () => SetContents(() => testSingle(5, autoplay)));
+ AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay)));
}
- private void testSingle(float circleSize, bool auto = false)
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestLongSpinner(bool autoplay)
{
- var spinner = new Spinner { StartTime = Time.Current + 1000, EndTime = Time.Current + 4000 };
+ AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 2000)));
+ AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
+ AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0));
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestSuperShortSpinner(bool autoplay)
+ {
+ AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 200)));
+ AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult);
+ AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1);
+ }
+
+ private Drawable testSingle(float circleSize, bool auto = false, double length = 3000)
+ {
+ const double delay = 2000;
+
+ var spinner = new Spinner
+ {
+ StartTime = Time.Current + delay,
+ EndTime = Time.Current + delay + length
+ };
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
- var drawable = new TestDrawableSpinner(spinner, auto)
+ drawableSpinner = new TestDrawableSpinner(spinner, auto)
{
Anchor = Anchor.Centre,
Depth = depthIndex++
};
foreach (var mod in SelectedMods.Value.OfType())
- mod.ApplyToDrawableHitObjects(new[] { drawable });
+ mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
- Add(drawable);
+ return drawableSpinner;
}
private class TestDrawableSpinner : DrawableSpinner
{
- private bool auto;
+ private readonly bool auto;
public TestDrawableSpinner(Spinner s, bool auto)
: base(s)
@@ -62,16 +81,11 @@ namespace osu.Game.Rulesets.Osu.Tests
this.auto = auto;
}
- protected override void CheckForResult(bool userTriggered, double timeOffset)
+ protected override void Update()
{
- if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1)
- {
- // force completion only once to not break human interaction
- Disc.RotationAbsolute = Spinner.SpinsRequired * 360;
- auto = false;
- }
-
- base.CheckForResult(userTriggered, timeOffset);
+ base.Update();
+ if (auto)
+ RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index ea006ec607..b46964e8b7 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -1,20 +1,29 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Utils;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osuTK;
-using System.Collections.Generic;
-using System.Linq;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
using osu.Game.Storyboards;
+using osu.Game.Tests.Visual;
+using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
@@ -28,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override bool Autoplay => true;
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
+
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
@@ -36,6 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
private DrawableSpinner drawableSpinner;
+ private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single();
[SetUpSteps]
public override void SetUpSteps()
@@ -49,24 +61,124 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerRewindingRotation()
{
+ double trackerRotationTolerance = 0;
+
addSeekStep(5000);
- AddAssert("is rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100));
+ AddStep("calculate rotation tolerance", () =>
+ {
+ trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
+ });
+ AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
+ AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
addSeekStep(0);
- AddAssert("is rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, 0, 100));
+ AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
+ AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
}
[Test]
public void TestSpinnerMiddleRewindingRotation()
{
- double estimatedRotation = 0;
+ double finalCumulativeTrackerRotation = 0;
+ double finalTrackerRotation = 0, trackerRotationTolerance = 0;
+ double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000);
- AddStep("retrieve rotation", () => estimatedRotation = drawableSpinner.Disc.RotationAbsolute);
+ AddStep("retrieve disc rotation", () =>
+ {
+ finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
+ trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
+ });
+ AddStep("retrieve spinner symbol rotation", () =>
+ {
+ finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
+ spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
+ });
+ AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.CumulativeRotation);
addSeekStep(2500);
+ AddAssert("disc rotation rewound",
+ // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
+ // due to the exponential damping applied we're allowing a larger margin of error of about 10%
+ // (5% relative to the final rotation value, but we're half-way through the spin).
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance));
+ AddAssert("symbol rotation rewound",
+ () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
+ AddAssert("is cumulative rotation rewound",
+ // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation / 2, 100));
+
addSeekStep(5000);
- AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100));
+ AddAssert("is disc rotation almost same",
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
+ AddAssert("is symbol rotation almost same",
+ () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
+ AddAssert("is cumulative rotation almost same",
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation, 100));
+ }
+
+ [Test]
+ public void TestRotationDirection([Values(true, false)] bool clockwise)
+ {
+ if (clockwise)
+ {
+ AddStep("flip replay", () =>
+ {
+ var drawableRuleset = this.ChildrenOfType().Single();
+ var score = drawableRuleset.ReplayScore;
+ var scoreWithFlippedReplay = new Score
+ {
+ ScoreInfo = score.ScoreInfo,
+ Replay = flipReplay(score.Replay)
+ };
+ drawableRuleset.SetReplayScore(scoreWithFlippedReplay);
+ });
+ }
+
+ addSeekStep(5000);
+
+ AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.RotationTracker.Rotation > 0 : drawableSpinner.RotationTracker.Rotation < 0);
+ AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
+ }
+
+ private Replay flipReplay(Replay scoreReplay) => new Replay
+ {
+ Frames = scoreReplay
+ .Frames
+ .Cast()
+ .Select(replayFrame =>
+ {
+ var flippedPosition = new Vector2(OsuPlayfield.BASE_SIZE.X - replayFrame.Position.X, replayFrame.Position.Y);
+ return new OsuReplayFrame(replayFrame.Time, flippedPosition, replayFrame.Actions.ToArray());
+ })
+ .Cast()
+ .ToList()
+ };
+
+ [Test]
+ public void TestSpinnerNormalBonusRewinding()
+ {
+ addSeekStep(1000);
+
+ AddAssert("player score matching expected bonus score", () =>
+ {
+ // multipled by 2 to nullify the score multiplier. (autoplay mod selected)
+ var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
+ return totalScore == (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK;
+ });
+
+ addSeekStep(0);
+
+ AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
+ }
+
+ [Test]
+ public void TestSpinnerCompleteBonusRewinding()
+ {
+ addSeekStep(2500);
+ addSeekStep(0);
+
+ AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
}
[Test]
@@ -74,13 +186,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
double estimatedSpm = 0;
- addSeekStep(2500);
+ addSeekStep(1000);
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
- addSeekStep(5000);
+ addSeekStep(2000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
- addSeekStep(2500);
+ addSeekStep(1000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
}
@@ -100,12 +212,17 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
EndTime = 6000,
},
- // placeholder object to avoid hitting the results screen
- new HitCircle
- {
- StartTime = 99999,
- }
}
};
+
+ private class ScoreExposedPlayer : TestPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ public ScoreExposedPlayer()
+ : base(false, false)
+ {
+ }
+ }
}
}
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 2fcfa1deb7..d6a68abaf2 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
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs
new file mode 100644
index 0000000000..9b33e746b3
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Judgements/OsuHitCircleJudgementResult.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Judgements
+{
+ public class OsuHitCircleJudgementResult : OsuJudgementResult
+ {
+ ///
+ /// The .
+ ///
+ public HitCircle HitCircle => (HitCircle)HitObject;
+
+ ///
+ /// The position of the player's cursor when was hit.
+ ///
+ public Vector2? CursorPositionAtHit;
+
+ public OsuHitCircleJudgementResult(HitObject hitObject, Judgement judgement)
+ : base(hitObject, judgement)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index d75f4c70d7..2263e2b2f4 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private OsuInputManager inputManager;
+ private GameplayClock gameplayClock;
+
private List replayFrames;
private int currentFrame;
@@ -38,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (currentFrame == replayFrames.Count - 1) return;
- double time = playfield.Time.Current;
+ double time = gameplayClock.CurrentTime;
// Very naive implementation of autopilot based on proximity to replay frames.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
@@ -53,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
+ gameplayClock = drawableRuleset.FrameStableClock;
+
// Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
index 73cb483ef0..ee6a7815e2 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Hit them at the right size!";
- protected override float StartScale => 2f;
+ [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
+ public override BindableNumber StartScale { get; } = new BindableFloat
+ {
+ MinValue = 1f,
+ MaxValue = 25f,
+ Default = 2f,
+ Value = 2f,
+ Precision = 0.1f,
+ };
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
index 8228161008..ff995e38ce 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public BindableNumber CircleSize { get; } = new BindableFloat
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public BindableNumber ApproachRate { get; } = new BindableFloat
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
index f08d4e8f5e..182d6eeb4b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Hit them at the right size!";
- protected override float StartScale => 0.5f;
+ [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
+ public override BindableNumber StartScale { get; } = new BindableFloat
+ {
+ MinValue = 0f,
+ MaxValue = 0.99f,
+ Default = 0.5f,
+ Value = 0.5f,
+ Precision = 0.01f,
+ };
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index fdba03f260..08fd13915d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -82,9 +82,7 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableSpinner spinner:
// hide elements we don't care about.
- spinner.Disc.Hide();
- spinner.Ticks.Hide();
- spinner.Background.Hide();
+ // todo: hide background
using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true))
spinner.FadeOut(fadeOutDuration);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index 42ddddc4dd..06ba4cde4a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
- protected virtual float StartScale => 1;
+ public abstract BindableNumber StartScale { get; }
protected virtual float EndScale => 1;
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableHitCircle _:
{
using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
- drawable.ScaleTo(StartScale).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);
+ drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 7b54baa99b..47d765fecd 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
var spinner = (DrawableSpinner)drawable;
- spinner.Disc.Tracking = true;
- spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
+ spinner.RotationTracker.Tracking = true;
+ spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index 774f9cf58b..d7582f3196 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods
var h = drawableOsu.HitObject;
+ //todo: expose and hide spinner background somehow
+
switch (drawable)
{
case DrawableHitCircle circle:
@@ -56,11 +58,6 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.Body.OnSkinChanged += () => applySliderState(slider);
applySliderState(slider);
break;
-
- case DrawableSpinner spinner:
- spinner.Disc.Hide();
- spinner.Background.Hide();
- break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index d73ad888f4..a438dc8be4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -7,12 +7,15 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
-using osuTK;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -32,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
+ private InputManager inputManager;
+
public DrawableHitCircle(HitCircle h)
: base(h)
{
@@ -86,6 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true);
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+ }
+
public override double LifetimeStart
{
get => base.LifetimeStart;
@@ -126,7 +138,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
}
- ApplyResult(r => r.Type = result);
+ ApplyResult(r =>
+ {
+ var circleResult = (OsuHitCircleJudgementResult)r;
+
+ // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
+ if (result != HitResult.Miss)
+ {
+ var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
+ circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
+ }
+
+ circleResult.Type = result;
+ });
}
protected override void UpdateInitialTransforms()
@@ -172,6 +196,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Drawable ProxiedLayer => ApproachCircle;
+ protected override JudgementResult CreateResult(Judgement judgement) => new OsuHitCircleJudgementResult(HitObject, judgement);
+
public class HitReceptor : CompositeDrawable, IKeyBindingHandler
{
// IsHovered is used
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 022e9ea12b..012d9f8878 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -16,52 +16,87 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableOsuJudgement : DrawableJudgement
{
- private SkinnableSprite lighting;
+ protected SkinnableSprite Lighting;
+
private Bindable lightingColour;
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
: base(result, judgedObject)
{
}
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
+ public DrawableOsuJudgement()
{
- if (config.Get(OsuSetting.HitLighting) && Result.Type != HitResult.Miss)
- {
- AddInternal(lighting = new SkinnableSprite("lighting")
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Blending = BlendingParameters.Additive,
- Depth = float.MaxValue
- });
+ }
- if (JudgedObject != null)
- {
- lightingColour = JudgedObject.AccentColour.GetBoundCopy();
- lightingColour.BindValueChanged(colour => lighting.Colour = colour.NewValue, true);
- }
- else
- {
- lighting.Colour = Color4.White;
- }
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(Lighting = new SkinnableSprite("lighting")
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Depth = float.MaxValue,
+ Alpha = 0
+ });
+ }
+
+ public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
+ {
+ base.Apply(result, judgedObject);
+
+ if (judgedObject?.HitObject is OsuHitObject osuObject)
+ {
+ Position = osuObject.StackedPosition;
+ Scale = new Vector2(osuObject.Scale);
}
}
- protected override double FadeOutDelay => lighting == null ? base.FadeOutDelay : 1400;
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
+ lightingColour?.UnbindAll();
+
+ Lighting.ResetAnimation();
+
+ if (JudgedObject != null)
+ {
+ lightingColour = JudgedObject.AccentColour.GetBoundCopy();
+ lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
+ }
+ else
+ {
+ Lighting.Colour = Color4.White;
+ }
+ }
+
+ private double fadeOutDelay;
+ protected override double FadeOutDelay => fadeOutDelay;
protected override void ApplyHitAnimations()
{
- if (lighting != null)
- {
- JudgementBody.Delay(FadeInDuration).FadeOut(400);
+ bool hitLightingEnabled = config.Get(OsuSetting.HitLighting);
- lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
- lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
+ if (hitLightingEnabled)
+ {
+ JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
+
+ Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
+ Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
+ }
+ else
+ {
+ JudgementBody.Alpha = 1;
}
- JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
+ fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
+
+ JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
base.ApplyHitAnimations();
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 72502c02cd..07f40f763b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
@@ -11,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
+using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
@@ -81,6 +83,42 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
}, true);
+
+ Tracking.BindValueChanged(updateSlidingSample);
+ }
+
+ private SkinnableSound slidingSample;
+
+ protected override void LoadSamples()
+ {
+ base.LoadSamples();
+
+ slidingSample?.Expire();
+ slidingSample = null;
+
+ var firstSample = HitObject.Samples.FirstOrDefault();
+
+ if (firstSample != null)
+ {
+ var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
+ clone.Name = "sliderslide";
+
+ AddInternal(slidingSample = new SkinnableSound(clone)
+ {
+ Looping = true
+ });
+ }
+ }
+
+ private void updateSlidingSample(ValueChangedEvent tracking)
+ {
+ // note that samples will not start playing if exiting a seek operation in the middle of a slider.
+ // may be something we want to address at a later point, but not so easy to make happen right now
+ // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
+ if (tracking.NewValue && ShouldPlaySamples)
+ slidingSample?.Play();
+ else
+ slidingSample?.Stop();
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
@@ -156,6 +194,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.Value = Ball.Tracking;
+ if (Tracking.Value && slidingSample != null)
+ // keep the sliding sample playing at the current tracking position
+ slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X);
+
double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
Ball.UpdateProgress(completionProgress);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 720ffcd51c..d79ecb7b4e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer;
+ public override bool DisplayResult => false;
+
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
: base(sliderRepeat)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 3c8ab0f5ab..7363da0de8 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -3,19 +3,18 @@
using System;
using System.Linq;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using osuTK;
-using osuTK.Graphics;
-using osu.Game.Graphics;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
+using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -23,27 +22,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
protected readonly Spinner Spinner;
- public readonly SpinnerDisc Disc;
- public readonly SpinnerTicks Ticks;
+ private readonly Container ticks;
+
+ public readonly SpinnerRotationTracker RotationTracker;
public readonly SpinnerSpmCounter SpmCounter;
-
- private readonly Container mainContainer;
-
- public readonly SpinnerBackground Background;
- private readonly Container circleContainer;
- private readonly CirclePiece circle;
- private readonly GlowPiece glow;
-
- private readonly SpriteIcon symbol;
-
- private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c");
- private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c");
+ private readonly SpinnerBonusDisplay bonusDisplay;
private readonly IBindable positionBindable = new Bindable();
- private Color4 normalColour;
- private Color4 completeColour;
-
public DrawableSpinner(Spinner s)
: base(s)
{
@@ -52,62 +38,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RelativeSizeAxes = Axes.Both;
- // we are slightly bigger than our parent, to clip the top and bottom of the circle
- Height = 1.3f;
-
Spinner = s;
InternalChildren = new Drawable[]
{
- circleContainer = new Container
- {
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new Drawable[]
- {
- glow = new GlowPiece(),
- circle = new CirclePiece
- {
- Position = Vector2.Zero,
- Anchor = Anchor.Centre,
- },
- new RingPiece(),
- symbol = new SpriteIcon
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(48),
- Icon = FontAwesome.Solid.Asterisk,
- Shadow = false,
- },
- }
- },
- mainContainer = new AspectContainer
+ ticks = new Container(),
+ new AspectContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
- Children = new[]
+ Children = new Drawable[]
{
- Background = new SpinnerBackground
- {
- Alpha = 0.6f,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- Disc = new SpinnerDisc(Spinner)
- {
- Scale = Vector2.Zero,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- circleContainer.CreateProxy(),
- Ticks = new SpinnerTicks
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
+ new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()),
+ RotationTracker = new SpinnerRotationTracker(Spinner)
}
},
SpmCounter = new SpinnerSpmCounter
@@ -116,51 +60,147 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre,
Y = 120,
Alpha = 0
+ },
+ bonusDisplay = new SpinnerBonusDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = -120,
}
};
}
+ private Bindable isSpinning;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ isSpinning = RotationTracker.IsSpinning.GetBoundCopy();
+ isSpinning.BindValueChanged(updateSpinningSample);
+ }
+
+ private SkinnableSound spinningSample;
+
+ private const float minimum_volume = 0.0001f;
+
+ protected override void LoadSamples()
+ {
+ base.LoadSamples();
+
+ spinningSample?.Expire();
+ spinningSample = null;
+
+ var firstSample = HitObject.Samples.FirstOrDefault();
+
+ if (firstSample != null)
+ {
+ var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
+ clone.Name = "spinnerspin";
+
+ AddInternal(spinningSample = new SkinnableSound(clone)
+ {
+ Volume = { Value = minimum_volume },
+ Looping = true,
+ });
+ }
+ }
+
+ private void updateSpinningSample(ValueChangedEvent tracking)
+ {
+ // note that samples will not start playing if exiting a seek operation in the middle of a spinner.
+ // may be something we want to address at a later point, but not so easy to make happen right now
+ // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
+ if (tracking.NewValue && ShouldPlaySamples)
+ {
+ spinningSample?.Play();
+ spinningSample?.VolumeTo(1, 200);
+ }
+ else
+ {
+ spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop());
+ }
+ }
+
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableSpinnerTick tick:
+ ticks.Add(tick);
+ break;
+ }
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ base.UpdateStateTransforms(state);
+
+ using (BeginDelayedSequence(Spinner.Duration, true))
+ this.FadeOut(160);
+
+ // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
+ isSpinning?.TriggerChange();
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ ticks.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case SpinnerBonusTick bonusTick:
+ return new DrawableSpinnerBonusTick(bonusTick);
+
+ case SpinnerTick tick:
+ return new DrawableSpinnerTick(tick);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- normalColour = baseColour;
-
- Background.AccentColour = normalColour;
-
- completeColour = colours.YellowLight.Opacity(0.75f);
-
- Disc.AccentColour = fillColour;
- circle.Colour = colours.BlueDark;
- glow.Colour = colours.BlueDark;
-
positionBindable.BindValueChanged(pos => Position = pos.NewValue);
positionBindable.BindTo(HitObject.PositionBindable);
}
- public float Progress => Math.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1);
+ ///
+ /// The completion progress of this spinner from 0..1 (clamped).
+ ///
+ public float Progress
+ {
+ get
+ {
+ if (Spinner.SpinsRequired == 0)
+ // some spinners are so short they can't require an integer spin count.
+ // these become implicitly hit.
+ return 1;
+
+ return Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
+ }
+ }
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Time.Current < HitObject.StartTime) return;
- if (Progress >= 1 && !Disc.Complete)
- {
- Disc.Complete = true;
-
- const float duration = 200;
-
- Disc.FadeAccent(completeColour, duration);
-
- Background.FadeAccent(completeColour, duration);
- Background.FadeOut(duration);
-
- circle.FadeColour(completeColour, duration);
- glow.FadeColour(completeColour, duration);
- }
+ RotationTracker.Complete.Value = Progress >= 1;
if (userTriggered || Time.Current < Spinner.EndTime)
return;
+ // Trigger a miss result for remaining ticks to avoid infinite gameplay.
+ foreach (var tick in ticks.Where(t => !t.IsHit))
+ tick.TriggerResult(false);
+
ApplyResult(r =>
{
if (Progress >= 1)
@@ -177,59 +217,55 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update()
{
base.Update();
+
if (HandleUserInput)
- Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
+ RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
+
+ if (spinningSample != null)
+ // todo: implement SpinnerFrequencyModulate
+ spinningSample.Frequency.Value = 0.5f + Progress;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
- if (!SpmCounter.IsPresent && Disc.Tracking)
+ if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
+ SpmCounter.SetRotation(RotationTracker.CumulativeRotation);
- circle.Rotation = Disc.Rotation;
- Ticks.Rotation = Disc.Rotation;
- SpmCounter.SetRotation(Disc.RotationAbsolute);
-
- float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
- Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint);
-
- symbol.RotateTo(Disc.Rotation / 2, 500, Easing.OutQuint);
+ updateBonusScore();
}
- protected override void UpdateInitialTransforms()
+ private int wholeSpins;
+
+ private void updateBonusScore()
{
- base.UpdateInitialTransforms();
+ if (ticks.Count == 0)
+ return;
- circleContainer.ScaleTo(Spinner.Scale * 0.3f);
- circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint);
+ int spins = (int)(RotationTracker.CumulativeRotation / 360);
- Disc.RotateTo(-720);
- symbol.RotateTo(-720);
-
- mainContainer
- .ScaleTo(0)
- .ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint)
- .Then()
- .ScaleTo(1, 500, Easing.OutQuint);
- }
-
- protected override void UpdateStateTransforms(ArmedState state)
- {
- base.UpdateStateTransforms(state);
-
- var sequence = this.Delay(Spinner.Duration).FadeOut(160);
-
- switch (state)
+ if (spins < wholeSpins)
{
- case ArmedState.Hit:
- sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out);
- break;
+ // rewinding, silently handle
+ wholeSpins = spins;
+ return;
+ }
- case ArmedState.Miss:
- sequence.ScaleTo(Scale * 0.8f, 320, Easing.In);
- break;
+ while (wholeSpins != spins)
+ {
+ var tick = ticks.FirstOrDefault(t => !t.IsHit);
+
+ // tick may be null if we've hit the spin limit.
+ if (tick != null)
+ {
+ tick.TriggerResult(true);
+ if (tick is DrawableSpinnerBonusTick)
+ bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired);
+ }
+
+ wholeSpins++;
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs
new file mode 100644
index 0000000000..2e1c07c4c6
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class DrawableSpinnerBonusTick : DrawableSpinnerTick
+ {
+ public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick)
+ : base(spinnerTick)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
new file mode 100644
index 0000000000..c390b673be
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class DrawableSpinnerTick : DrawableOsuHitObject
+ {
+ public override bool DisplayResult => false;
+
+ public DrawableSpinnerTick(SpinnerTick spinnerTick)
+ : base(spinnerTick)
+ {
+ }
+
+ ///
+ /// Apply a judgement result.
+ ///
+ /// Whether this tick was reached.
+ internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
new file mode 100644
index 0000000000..dfb692eba9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
@@ -0,0 +1,189 @@
+// 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.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
+{
+ public class DefaultSpinnerDisc : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner;
+
+ private Spinner spinner;
+
+ private const float idle_alpha = 0.2f;
+ private const float tracking_alpha = 0.4f;
+
+ private Color4 normalColour;
+ private Color4 completeColour;
+
+ private SpinnerTicks ticks;
+
+ private int wholeRotationCount;
+
+ private SpinnerFill fill;
+ private Container mainContainer;
+ private SpinnerCentreLayer centre;
+ private SpinnerBackgroundLayer background;
+
+ public DefaultSpinnerDisc()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ // we are slightly bigger than our parent, to clip the top and bottom of the circle
+ // this should probably be revisited when scaled spinners are a thing.
+ Scale = new Vector2(1.3f);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, DrawableHitObject drawableHitObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableHitObject;
+ spinner = (Spinner)drawableSpinner.HitObject;
+
+ normalColour = colours.BlueDark;
+ completeColour = colours.YellowLight;
+
+ InternalChildren = new Drawable[]
+ {
+ mainContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ background = new SpinnerBackgroundLayer(),
+ fill = new SpinnerFill
+ {
+ Alpha = idle_alpha,
+ AccentColour = normalColour
+ },
+ ticks = new SpinnerTicks
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AccentColour = normalColour
+ },
+ }
+ },
+ centre = new SpinnerCentreLayer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (drawableSpinner.RotationTracker.Complete.Value)
+ {
+ if (checkNewRotationCount)
+ {
+ fill.FinishTransforms(false, nameof(Alpha));
+ fill
+ .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
+ .Then()
+ .FadeTo(tracking_alpha, 250, Easing.OutQuint);
+ }
+ }
+ else
+ {
+ fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
+ }
+
+ const float initial_scale = 0.2f;
+ float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress;
+
+ fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
+ mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ centre.ScaleTo(0);
+ mainContainer.ScaleTo(0);
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ {
+ // constant ambient rotation to give the spinner "spinning" character.
+ this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+
+ centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
+ mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ {
+ centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
+ mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ }
+ }
+
+ // transforms we have from completing the spinner will be rolled back, so reapply immediately.
+ updateComplete(state.NewValue == ArmedState.Hit, 0);
+
+ using (BeginDelayedSequence(spinner.Duration, true))
+ {
+ switch (state.NewValue)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
+ this.RotateTo(mainContainer.Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ this.ScaleTo(Scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
+ }
+
+ private void updateComplete(bool complete, double duration)
+ {
+ var colour = complete ? completeColour : normalColour;
+
+ ticks.FadeAccent(colour.Darken(1), duration);
+ fill.FadeAccent(colour.Darken(1), duration);
+
+ background.FadeAccent(colour, duration);
+ centre.FadeAccent(colour, duration);
+ }
+
+ private bool checkNewRotationCount
+ {
+ get
+ {
+ int rotations = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360);
+
+ if (wholeRotationCount == rotations) return false;
+
+ wholeRotationCount = rotations;
+ return true;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index 395c76a233..07dc6021c9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly Slider slider;
private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider;
- private readonly CircularContainer ball;
+ private readonly Drawable ball;
public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{
@@ -54,19 +54,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Alpha = 0,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
},
- ball = new CircularContainer
+ ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
{
- Masking = true,
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Alpha = 1,
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()),
- }
- }
+ Origin = Anchor.Centre,
+ },
};
}
@@ -187,12 +179,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return;
Position = newPos;
- 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);
lastPosition = newPos;
}
- private class FollowCircleContainer : Container
+ private class FollowCircleContainer : CircularContainer
{
public override bool HandlePositionalInput => true;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
new file mode 100644
index 0000000000..b499d7a92b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
+{
+ ///
+ /// Shows incremental bonus score achieved for a spinner.
+ ///
+ public class SpinnerBonusDisplay : CompositeDrawable
+ {
+ private readonly OsuSpriteText bonusCounter;
+
+ public SpinnerBonusDisplay()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChild = bonusCounter = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Numeric.With(size: 24),
+ Alpha = 0,
+ };
+ }
+
+ private int displayedCount;
+
+ public void SetBonusCount(int count)
+ {
+ if (displayedCount == count)
+ return;
+
+ displayedCount = count;
+ bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}";
+ bonusCounter.FadeOutFromOne(1500);
+ bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
similarity index 85%
rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
rename to osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
index 77228e28af..043bc5618c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
@@ -1,18 +1,18 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SpinnerBackground : CircularContainer, IHasAccentColour
+ public class SpinnerFill : CircularContainer, IHasAccentColour
{
- protected Box Disc;
+ public readonly Box Disc;
public Color4 AccentColour
{
@@ -31,11 +31,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
}
- public SpinnerBackground()
+ public SpinnerFill()
{
RelativeSizeAxes = Axes.Both;
Masking = true;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
Children = new Drawable[]
{
Disc = new Box
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
similarity index 54%
rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
rename to osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
index d4ef039b79..0cc6c842f4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
@@ -2,76 +2,51 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osuTK;
-using osuTK.Graphics;
using osu.Framework.Utils;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SpinnerDisc : CircularContainer, IHasAccentColour
+ public class SpinnerRotationTracker : CircularContainer
{
private readonly Spinner spinner;
- public Color4 AccentColour
- {
- get => background.AccentColour;
- set => background.AccentColour = value;
- }
-
- private readonly SpinnerBackground background;
-
- private const float idle_alpha = 0.2f;
- private const float tracking_alpha = 0.4f;
-
public override bool IsPresent => true; // handle input when hidden
- public SpinnerDisc(Spinner s)
+ public SpinnerRotationTracker(Spinner s)
{
spinner = s;
RelativeSizeAxes = Axes.Both;
-
- Children = new Drawable[]
- {
- background = new SpinnerBackground { Alpha = idle_alpha },
- };
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
- private bool tracking;
+ public bool Tracking { get; set; }
- public bool Tracking
- {
- get => tracking;
- set
- {
- if (value == tracking) return;
+ public readonly BindableBool Complete = new BindableBool();
- tracking = value;
+ ///
+ /// The total rotation performed on the spinner disc, disregarding the spin direction.
+ ///
+ ///
+ /// This value is always non-negative and is monotonically increasing with time
+ /// (i.e. will only increase if time is passing forward, but can decrease during rewind).
+ ///
+ ///
+ /// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
+ /// this property will return the value of 720 (as opposed to 0 for ).
+ ///
+ public float CumulativeRotation { get; private set; }
- background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100);
- }
- }
-
- private bool complete;
-
- public bool Complete
- {
- get => complete;
- set
- {
- if (value == complete) return;
-
- complete = value;
-
- updateCompleteTick();
- }
- }
+ ///
+ /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
+ ///
+ public readonly BindableBool IsSpinning = new BindableBool();
///
/// Whether currently in the correct time range to allow spinning.
@@ -88,10 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private float lastAngle;
private float currentRotation;
- public float RotationAbsolute;
- private int completeTick;
-
- private bool updateCompleteTick() => completeTick != (completeTick = (int)(RotationAbsolute / 360));
private bool rotationTransferred;
@@ -102,21 +73,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
var delta = thisAngle - lastAngle;
- if (tracking)
- Rotate(delta);
+ if (Tracking)
+ AddRotation(delta);
lastAngle = thisAngle;
- if (Complete && updateCompleteTick())
- {
- background.FinishTransforms(false, nameof(Alpha));
- background
- .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
- .Then()
- .FadeTo(tracking_alpha, 250, Easing.OutQuint);
- }
+ IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
- Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
+ Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed));
}
///
@@ -126,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
/// Will be a no-op if not a valid time to spin.
///
/// The delta angle.
- public void Rotate(float angle)
+ public void AddRotation(float angle)
{
if (!isSpinnableTime)
return;
@@ -149,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
currentRotation += angle;
- RotationAbsolute += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
+ CumulativeRotation += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs
index 676cefb236..ba7e8eae6f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -9,10 +10,11 @@ using osu.Framework.Graphics.Effects;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SpinnerTicks : Container
+ public class SpinnerTicks : Container, IHasAccentColour
{
public SpinnerTicks()
{
@@ -20,28 +22,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
- const float count = 18;
+ const float count = 8;
for (float i = 0; i < count; i++)
{
Add(new Container
{
- Colour = Color4.Black,
Alpha = 0.4f,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Radius = 10,
- Colour = Color4.Gray.Opacity(0.2f),
- },
+ Blending = BlendingParameters.Additive,
RelativePositionAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Size = new Vector2(60, 10),
Origin = Anchor.Centre,
Position = new Vector2(
- 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.86f,
- 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.86f
+ 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.83f,
+ 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.83f
),
Rotation = -i / count * 360 + 90,
Children = new[]
@@ -54,5 +50,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
});
}
}
+
+ public Color4 AccentColour
+ {
+ get => Colour;
+ set
+ {
+ Colour = value;
+
+ foreach (var c in Children.OfType())
+ {
+ c.EdgeEffect =
+ new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 20,
+ Colour = value.Opacity(0.8f),
+ };
+ }
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs
new file mode 100644
index 0000000000..3cd2454706
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class SpinnerBackgroundLayer : SpinnerFill
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, DrawableHitObject drawableHitObject)
+ {
+ Disc.Alpha = 0;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs
new file mode 100644
index 0000000000..b62ce822f0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class SpinnerCentreLayer : CompositeDrawable, IHasAccentColour
+ {
+ private DrawableSpinner spinner;
+
+ private CirclePiece circle;
+ private GlowPiece glow;
+ private SpriteIcon symbol;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ spinner = (DrawableSpinner)drawableHitObject;
+
+ InternalChildren = new Drawable[]
+ {
+ glow = new GlowPiece(),
+ circle = new CirclePiece
+ {
+ Position = Vector2.Zero,
+ Anchor = Anchor.Centre,
+ },
+ new RingPiece(),
+ symbol = new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(48),
+ Icon = FontAwesome.Solid.Asterisk,
+ Shadow = false,
+ },
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, spinner.RotationTracker.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ accentColour = value;
+
+ circle.Colour = accentColour;
+ glow.Colour = accentColour;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index c11e20c9e7..1e54b576f1 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
@@ -4,6 +4,7 @@
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
@@ -24,6 +25,13 @@ namespace osu.Game.Rulesets.Osu.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
- public override Judgement CreateJudgement() => new SliderRepeat.SliderRepeatJudgement();
+ public override Judgement CreateJudgement() => new SliderTailJudgement();
+
+ public class SliderTailJudgement : OsuJudgement
+ {
+ protected override int NumericResultFor(HitResult result) => 0;
+
+ public override bool AffectsCombo => false;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 418375c090..1658a4e7c2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -1,11 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
@@ -26,14 +25,43 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public int SpinsRequired { get; protected set; } = 1;
+ ///
+ /// Number of spins available to give bonus, beyond .
+ ///
+ public int MaximumBonusSpins { get; protected set; } = 1;
+
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
- SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5));
-
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
- SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6);
+ const double stable_matching_fudge = 0.6;
+
+ // close to 477rpm
+ const double maximum_rotations_per_second = 8;
+
+ double secondsDuration = Duration / 1000;
+
+ double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
+
+ SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
+ MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
+ }
+
+ protected override void CreateNestedHitObjects()
+ {
+ base.CreateNestedHitObjects();
+
+ int totalSpins = MaximumBonusSpins + SpinsRequired;
+
+ for (int i = 0; i < totalSpins; i++)
+ {
+ double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
+
+ AddNested(i < SpinsRequired
+ ? new SpinnerTick { StartTime = startTime }
+ : new SpinnerBonusTick { StartTime = startTime });
+ }
}
public override Judgement CreateJudgement() => new OsuJudgement();
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
new file mode 100644
index 0000000000..9c4b6f774f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Audio;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ public class SpinnerBonusTick : SpinnerTick
+ {
+ public new const int SCORE_PER_TICK = 50;
+
+ public SpinnerBonusTick()
+ {
+ Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
+ }
+
+ public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
+
+ public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
+ {
+ protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
+
+ protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
new file mode 100644
index 0000000000..de3ae27e55
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ public class SpinnerTick : OsuHitObject
+ {
+ public const int SCORE_PER_TICK = 10;
+
+ public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
+
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ public class OsuSpinnerTickJudgement : OsuJudgement
+ {
+ public override bool AffectsCombo => false;
+
+ protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
+
+ protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 689a7b35ea..eaa5d8937a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -29,9 +29,15 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using System;
+using System.Linq;
+using osu.Framework.Testing;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Statistics;
+using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu
{
+ [ExcludeFromDynamicCompile]
public class OsuRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
@@ -186,5 +192,31 @@ namespace osu.Game.Rulesets.Osu
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
+
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ {
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList())
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ },
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ }
+ };
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index b2cdc8ccbf..5468764692 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -17,5 +17,6 @@ namespace osu.Game.Rulesets.Osu
SliderFollowCircle,
SliderBall,
SliderBody,
+ SpinnerBody
}
}
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index 9ab358ee12..3356a0fbe0 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Replays
///
protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2;
- protected const float SPIN_RADIUS = 50;
+ public const float SPIN_RADIUS = 50;
///
/// The time in ms between each ReplayFrame.
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
index b42e9ac187..cf48dc053f 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
@@ -36,19 +36,10 @@ namespace osu.Game.Rulesets.Osu.Replays
}
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- return new List
- {
- new MousePositionAbsoluteInput
- {
- Position = GamefieldToScreenSpace(Position ?? Vector2.Zero)
- },
- new ReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List()
- }
- };
+ inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) });
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 79a6ea7e92..86ec76e373 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -4,13 +4,27 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuScoreProcessor : ScoreProcessor
{
- protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement);
+ protected override HitEvent CreateHitEvent(JudgementResult result)
+ => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
+
+ protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
+ {
+ switch (hitObject)
+ {
+ case HitCircle _:
+ return new OsuHitCircleJudgementResult(hitObject, judgement);
+
+ default:
+ return new OsuJudgementResult(hitObject, judgement);
+ }
+ }
public override HitWindows CreateHitWindows() => new OsuHitWindows();
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
index e7486ef9b0..0ab3e8825b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Rulesets.Osu.Skinning
{
@@ -28,53 +29,67 @@ namespace osu.Game.Rulesets.Osu.Skinning
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
+ private Container circleSprites;
+ private Sprite hitCircleSprite;
+ private Sprite hitCircleOverlay;
+
+ private SkinnableSpriteText hitCircleText;
+
private readonly IBindable state = new Bindable();
private readonly Bindable accentColour = new Bindable();
private readonly IBindable indexInCurrentCombo = new Bindable();
+ [Resolved]
+ private ISkinSource skin { get; set; }
+
[BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ private void load(DrawableHitObject drawableObject)
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
- Sprite hitCircleSprite;
- SkinnableSpriteText hitCircleText;
-
InternalChildren = new Drawable[]
{
- hitCircleSprite = new Sprite
+ circleSprites = new Container
{
- Texture = getTextureWithFallback(string.Empty),
- Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ hitCircleSprite = new Sprite
+ {
+ Texture = getTextureWithFallback(string.Empty),
+ Colour = drawableObject.AccentColour.Value,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ hitCircleOverlay = new Sprite
+ {
+ Texture = getTextureWithFallback("overlay"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ }
},
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
- }, confineMode: ConfineMode.NoScaling),
- new Sprite
+ }, confineMode: ConfineMode.NoScaling)
{
- Texture = getTextureWithFallback("overlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- }
+ },
};
bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
- if (!overlayAboveNumber)
- ChangeInternalChildDepth(hitCircleText, -float.MaxValue);
+ if (overlayAboveNumber)
+ AddInternal(hitCircleOverlay.CreateProxy());
state.BindTo(drawableObject.State);
- state.BindValueChanged(updateState, true);
-
accentColour.BindTo(drawableObject.AccentColour);
- accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
-
indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
- indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
Texture getTextureWithFallback(string name)
{
@@ -87,6 +102,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ state.BindValueChanged(updateState, true);
+ accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
+ indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
+ }
+
private void updateState(ValueChangedEvent state)
{
const double legacy_fade_duration = 240;
@@ -94,8 +118,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
switch (state.NewValue)
{
case ArmedState.Hit:
- this.FadeOut(legacy_fade_duration, Easing.Out);
- this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
+ circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+
+ var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value;
+
+ if (legacyVersion >= 2.0m)
+ // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
+ hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
+ else
+ {
+ // old skins scale and fade it normally along other pieces.
+ hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ }
+
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
new file mode 100644
index 0000000000..72bc3ddc9a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
@@ -0,0 +1,99 @@
+// 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.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// Legacy skinned spinner with two main spinning layers, one fixed overlay and one final spinning overlay.
+ /// No background layer.
+ ///
+ public class LegacyNewStyleSpinner : CompositeDrawable
+ {
+ private Sprite discBottom;
+ private Sprite discTop;
+ private Sprite spinningMiddle;
+ private Sprite fixedMiddle;
+
+ private DrawableSpinner drawableSpinner;
+
+ private const float final_scale = 0.625f;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource source, DrawableHitObject drawableObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableObject;
+
+ Scale = new Vector2(final_scale);
+
+ InternalChildren = new Drawable[]
+ {
+ discBottom = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-bottom")
+ },
+ discTop = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-top")
+ },
+ fixedMiddle = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-middle")
+ },
+ spinningMiddle = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-middle2")
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.FadeOut();
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ var spinner = (Spinner)drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ this.FadeInFromZero(spinner.TimePreempt / 2);
+
+ fixedMiddle.FadeColour(Color4.White);
+ using (BeginAbsoluteSequence(spinner.StartTime, true))
+ fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation;
+ discBottom.Rotation = discTop.Rotation / 3;
+
+ Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
new file mode 100644
index 0000000000..0ae1d8f683
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// Legacy skinned spinner with one main spinning layer and a background layer.
+ ///
+ public class LegacyOldStyleSpinner : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner;
+ private Sprite disc;
+ private Container metre;
+
+ private const float background_y_offset = 20;
+
+ private const float sprite_scale = 1 / 1.6f;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource source, DrawableHitObject drawableObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableObject;
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Sprite
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Texture = source.GetTexture("spinner-background"),
+ Y = background_y_offset,
+ Scale = new Vector2(sprite_scale)
+ },
+ disc = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-circle"),
+ Scale = new Vector2(sprite_scale)
+ },
+ metre = new Container
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Y = background_y_offset,
+ Masking = true,
+ Child = new Sprite
+ {
+ Texture = source.GetTexture("spinner-metre"),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ },
+ Scale = new Vector2(0.625f)
+ }
+ };
+ }
+
+ private Vector2 metreFinalSize;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.FadeOut();
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+
+ metreFinalSize = metre.Size = metre.Child.Size;
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ var spinner = drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ this.FadeInFromZero(spinner.TimePreempt / 2);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ disc.Rotation = drawableSpinner.RotationTracker.Rotation;
+ metre.Height = getMetreHeight(drawableSpinner.Progress);
+ }
+
+ private const int total_bars = 10;
+
+ private float getMetreHeight(float progress)
+ {
+ progress = Math.Min(99, progress * 100);
+
+ int barCount = (int)progress / 10;
+
+ // todo: add SpinnerNoBlink support
+ if (RNG.NextBool(((int)progress % 10) / 10f))
+ barCount++;
+
+ return (float)barCount / total_bars * metreFinalSize.Y;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
index b4ed75d97c..0f586034d5 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
@@ -15,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
private readonly Drawable animationContent;
+ private Sprite layerNd;
+ private Sprite layerSpec;
+
public LegacySliderBall(Drawable animationContent)
{
this.animationContent = animationContent;
@@ -29,18 +32,37 @@ namespace osu.Game.Rulesets.Osu.Skinning
InternalChildren = new[]
{
- new Sprite
+ layerNd = new Sprite
{
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255),
},
- animationContent,
- new Sprite
+ animationContent.With(d =>
{
+ d.Anchor = Anchor.Centre;
+ d.Origin = Anchor.Centre;
+ }),
+ layerSpec = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-spec"),
Blending = BlendingParameters.Additive,
},
};
}
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ //undo rotation on layers which should not be rotated.
+ float appliedRotation = Parent.Rotation;
+
+ layerNd.Rotation = -appliedRotation;
+ layerSpec.Rotation = -appliedRotation;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 3e5758ca01..81d1d05b66 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
case OsuSkinComponents.HitCircleText:
var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
- var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0;
+ var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
return !hasFont(font)
? null
@@ -102,6 +102,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
Scale = new Vector2(0.8f),
Spacing = new Vector2(-overlap, 0)
};
+
+ case OsuSkinComponents.SpinnerBody:
+ bool hasBackground = Source.GetTexture("spinner-background") != null;
+
+ if (Source.GetTexture("spinner-top") != null && !hasBackground)
+ return new LegacyNewStyleSpinner();
+ else if (hasBackground)
+ return new LegacyOldStyleSpinner();
+
+ return null;
}
return null;
diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
new file mode 100644
index 0000000000..20adbc1c02
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs
@@ -0,0 +1,297 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Statistics
+{
+ public class AccuracyHeatmap : CompositeDrawable
+ {
+ ///
+ /// Size of the inner circle containing the "hit" points, relative to the size of this .
+ /// All other points outside of the inner circle are "miss" points.
+ ///
+ private const float inner_portion = 0.8f;
+
+ ///
+ /// Number of rows/columns of points.
+ /// ~4px per point @ 128x128 size (the contents of the are always square). 1089 total points.
+ ///
+ private const int points_per_dimension = 33;
+
+ private const float rotation = 45;
+
+ private BufferedContainer bufferedGrid;
+ private GridContainer pointGrid;
+
+ private readonly ScoreInfo score;
+ private readonly IBeatmap playableBeatmap;
+
+ private const float line_thickness = 2;
+
+ ///
+ /// The highest count of any point currently being displayed.
+ ///
+ protected float PeakValue { get; private set; }
+
+ public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap)
+ {
+ this.score = score;
+ this.playableBeatmap = playableBeatmap;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Children = new Drawable[]
+ {
+ new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(inner_portion),
+ Masking = true,
+ BorderThickness = line_thickness,
+ BorderColour = Color4.White,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex("#202624")
+ }
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding(1),
+ Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ EdgeSmoothness = new Vector2(1),
+ RelativeSizeAxes = Axes.Y,
+ Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
+ Width = line_thickness / 2,
+ Rotation = -rotation,
+ Alpha = 0.3f,
+ },
+ new Box
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ EdgeSmoothness = new Vector2(1),
+ RelativeSizeAxes = Axes.Y,
+ Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
+ Width = line_thickness / 2, // adjust for edgesmoothness
+ Rotation = rotation
+ },
+ }
+ },
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Width = 10,
+ EdgeSmoothness = new Vector2(1),
+ Height = line_thickness / 2, // adjust for edgesmoothness
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ EdgeSmoothness = new Vector2(1),
+ Width = line_thickness / 2, // adjust for edgesmoothness
+ Height = 10,
+ }
+ }
+ },
+ bufferedGrid = new BufferedContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CacheDrawnFrameBuffer = true,
+ BackgroundColour = Color4Extensions.FromHex("#202624").Opacity(0),
+ Child = pointGrid = new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ }
+ };
+
+ Vector2 centre = new Vector2(points_per_dimension) / 2;
+ float innerRadius = centre.X * inner_portion;
+
+ Drawable[][] points = new Drawable[points_per_dimension][];
+
+ for (int r = 0; r < points_per_dimension; r++)
+ {
+ points[r] = new Drawable[points_per_dimension];
+
+ for (int c = 0; c < points_per_dimension; c++)
+ {
+ HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius
+ ? HitPointType.Hit
+ : HitPointType.Miss;
+
+ var point = new HitPoint(pointType, this)
+ {
+ Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255)
+ };
+
+ points[r][c] = point;
+ }
+ }
+
+ pointGrid.Content = points;
+
+ if (score.HitEvents == null || score.HitEvents.Count == 0)
+ return;
+
+ // Todo: This should probably not be done like this.
+ float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2;
+
+ foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
+ {
+ if (e.LastHitObject == null || e.Position == null)
+ continue;
+
+ AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value, radius);
+ }
+ }
+
+ protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
+ {
+ if (pointGrid.Content.Length == 0)
+ return;
+
+ double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
+ double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point.
+ double finalAngle = angle2 - angle1; // Angle between start, end, and hit points.
+ float normalisedDistance = Vector2.Distance(hitPoint, end) / radius;
+
+ // Consider two objects placed horizontally, with the start on the left and the end on the right.
+ // The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form:
+ // +pi | 0
+ // O --------- O -----> Note: Math.Atan2 has a range (-pi <= theta <= +pi)
+ // -pi | 0
+ // E.g. If the hit point was directly above end, it would have an angle pi/2.
+ //
+ // It also calculated the angle separating hitPoint from the line joining {start, end}, that is anti-clockwise in the form:
+ // 0 | pi
+ // O --------- O ----->
+ // 2pi | pi
+ //
+ // However keep in mind that cos(0)=1 and cos(2pi)=1, whereas we actually want these values to appear on the left, so the x-coordinate needs to be inverted.
+ // Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
+ //
+ // We also need to apply the anti-clockwise rotation.
+ var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
+ var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
+
+ Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
+ float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies.
+ Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
+
+ // Find the most relevant hit point.
+ int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1);
+ int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1);
+
+ PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment());
+
+ bufferedGrid.ForceRedraw();
+ }
+
+ private class HitPoint : Circle
+ {
+ private readonly HitPointType pointType;
+ private readonly AccuracyHeatmap heatmap;
+
+ public override bool IsPresent => count > 0;
+
+ public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap)
+ {
+ this.pointType = pointType;
+ this.heatmap = heatmap;
+
+ RelativeSizeAxes = Axes.Both;
+ Alpha = 1;
+ }
+
+ private int count;
+
+ ///
+ /// Increment the value of this point by one.
+ ///
+ /// The value after incrementing.
+ public int Increment()
+ {
+ return ++count;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // the point at which alpha is saturated and we begin to adjust colour lightness.
+ const float lighten_cutoff = 0.95f;
+
+ // the amount of lightness to attribute regardless of relative value to peak point.
+ const float non_relative_portion = 0.2f;
+
+ float amount = 0;
+
+ // give some amount of alpha regardless of relative count
+ amount += non_relative_portion * Math.Min(1, count / 10f);
+
+ // add relative portion
+ amount += (1 - non_relative_portion) * (count / heatmap.PeakValue);
+
+ // apply easing
+ amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount));
+
+ Debug.Assert(amount <= 1);
+
+ Alpha = Math.Min(amount / lighten_cutoff, 1);
+ if (pointType == HitPointType.Hit)
+ Colour = ((Color4)Colour).Lighten(Math.Max(0, amount - lighten_cutoff));
+ }
+ }
+
+ private enum HitPointType
+ {
+ Hit,
+ Miss
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
index 4f3d07f208..eea45c6c80 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
@@ -59,10 +59,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
if (!cursorExpand) return;
- expandTarget.ScaleTo(released_scale).ScaleTo(pressed_scale, 100, Easing.OutQuad);
+ expandTarget.ScaleTo(released_scale).ScaleTo(pressed_scale, 400, Easing.OutElasticHalf);
}
- public void Contract() => expandTarget.ScaleTo(released_scale, 100, Easing.OutQuad);
+ public void Contract() => expandTarget.ScaleTo(released_scale, 400, Easing.OutQuad);
private class DefaultCursor : OsuCursorSprite
{
@@ -115,24 +115,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
},
},
},
- new CircularContainer
- {
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Scale = new Vector2(0.1f),
- Masking = true,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.White,
- },
- },
- },
- }
- }
+ },
+ },
+ new Circle
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Scale = new Vector2(0.14f),
+ Colour = new Color4(34, 93, 204, 255),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 8,
+ Colour = Color4.White,
+ },
+ },
};
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
index 28600ef55b..5812e8cf75 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs
@@ -30,7 +30,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Drawable cursorTrail;
- public Bindable CursorScale = new BindableFloat(1);
+ public IBindable CursorScale => cursorScale;
+
+ private readonly Bindable cursorScale = new BindableFloat(1);
private Bindable userCursorScale;
private Bindable autoCursorScale;
@@ -68,13 +70,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize);
autoCursorScale.ValueChanged += _ => calculateScale();
- CursorScale.ValueChanged += e =>
+ CursorScale.BindValueChanged(e =>
{
var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale;
cursorTrail.Scale = newScale;
- };
+ }, true);
calculateScale();
}
@@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
}
- CursorScale.Value = scale;
+ cursorScale.Value = scale;
var newScale = new Vector2(scale);
diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
index b4d51d11c9..b2299398e1 100644
--- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);
- public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer();
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true };
protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay();
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 4b1a2ce43c..4ef9bbe091 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -1,23 +1,30 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK;
+using System;
+using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
-using osu.Game.Rulesets.UI;
-using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.UI.Cursor;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
public class OsuPlayfield : Playfield
{
- private readonly ApproachCircleProxyContainer approachCircles;
+ private readonly ProxyContainer approachCircles;
+ private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
private readonly FollowPointRenderer followPoints;
private readonly OrderedHitPolicy hitPolicy;
@@ -26,10 +33,16 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
+ private readonly IDictionary> poolDictionary = new Dictionary>();
+
public OsuPlayfield()
{
InternalChildren = new Drawable[]
{
+ spinnerProxies = new ProxyContainer
+ {
+ RelativeSizeAxes = Axes.Both
+ },
followPoints = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
@@ -46,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
Child = HitObjectContainer,
},
- approachCircles = new ApproachCircleProxyContainer
+ approachCircles = new ProxyContainer
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
@@ -54,6 +67,13 @@ namespace osu.Game.Rulesets.Osu.UI
};
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
+
+ var hitWindows = new OsuHitWindows();
+
+ foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
+ poolDictionary.Add(result, new DrawableJudgementPool(result));
+
+ AddRangeInternal(poolDictionary.Values);
}
public override void Add(DrawableHitObject h)
@@ -61,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.UI
h.OnNewResult += onNewResult;
h.OnLoadComplete += d =>
{
+ if (d is DrawableSpinner)
+ spinnerProxies.Add(d.CreateProxy());
+
if (d is IDrawableHitObjectWithProxiedApproach c)
approachCircles.Add(c.ProxiedLayer.CreateProxy());
};
@@ -91,21 +114,37 @@ namespace osu.Game.Rulesets.Osu.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- DrawableOsuJudgement explosion = new DrawableOsuJudgement(result, judgedObject)
- {
- Origin = Anchor.Centre,
- Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition,
- Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale)
- };
+ DrawableOsuJudgement explosion = poolDictionary[result.Type].Get(doj => doj.Apply(result, judgedObject));
judgementLayer.Add(explosion);
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
- private class ApproachCircleProxyContainer : LifetimeManagementContainer
+ private class ProxyContainer : LifetimeManagementContainer
{
- public void Add(Drawable approachCircleProxy) => AddInternal(approachCircleProxy);
+ public void Add(Drawable proxy) => AddInternal(proxy);
+ }
+
+ private class DrawableJudgementPool : DrawablePool
+ {
+ private readonly HitResult result;
+
+ public DrawableJudgementPool(HitResult result)
+ : base(10)
+ {
+ this.result = result;
+ }
+
+ protected override DrawableOsuJudgement CreateNewDrawable()
+ {
+ var judgement = base.CreateNewDrawable();
+
+ // just a placeholder to initialise the correct drawable hierarchy for this pool.
+ judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
+
+ return judgement;
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
index 9c8be868b0..0d1a5a8304 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs
@@ -11,10 +11,19 @@ namespace osu.Game.Rulesets.Osu.UI
public class OsuPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
protected override Container Content => content;
- private readonly Container content;
+ private readonly ScalingContainer content;
private const float playfield_size_adjust = 0.8f;
+ ///
+ /// When true, an offset is applied to allow alignment with historical storyboards displayed in the same parent space.
+ /// This will shift the playfield downwards slightly.
+ ///
+ public bool AlignWithStoryboard
+ {
+ set => content.PlayfieldShift = value;
+ }
+
public OsuPlayfieldAdjustmentContainer()
{
Anchor = Anchor.Centre;
@@ -39,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.UI
///
private class ScalingContainer : Container
{
+ internal bool PlayfieldShift { get; set; }
+
protected override void Update()
{
base.Update();
@@ -55,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI
// Scale = 819.2 / 512
// Scale = 1.6
Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
+ Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Scale.X);
// Size = 0.625
Size = Vector2.Divide(Vector2.One, Scale);
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index abba444c73..ec7751d2b4 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI
private OsuClickToResumeCursor clickToResumeCursor;
private OsuCursorContainer localCursorContainer;
- private Bindable localCursorScale;
+ private IBindable localCursorScale;
public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu
new file mode 100644
index 0000000000..f9755782c2
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu
@@ -0,0 +1,10 @@
+osu file format v14
+
+[General]
+Mode: 1
+
+[TimingPoints]
+0,300,4,1,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png
new file mode 100644
index 0000000000..c5bcdbd3fc
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png
new file mode 100644
index 0000000000..39cf737285
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonidle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonidle.png
new file mode 100644
index 0000000000..4c3b2bfec9
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonidle.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png
new file mode 100644
index 0000000000..7de00b5390
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
index d200c44a02..47d8a5c012 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -36,6 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private TaikoScoreProcessor scoreProcessor;
private IEnumerable mascots => this.ChildrenOfType();
+
+ private IEnumerable animatedMascots =>
+ mascots.Where(mascot => mascot.ChildrenOfType().All(animation => animation.FrameCount > 0));
+
private IEnumerable playfields => this.ChildrenOfType();
[SetUp]
@@ -72,11 +77,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }));
- AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear));
+ AddAssert("skins with animations remain in clear state", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail));
AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
- AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear));
+ AddAssert("skins with animations change to clear", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
}
[Test]
@@ -111,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
- assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Fail);
+ assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle);
}
@@ -186,10 +191,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
{
- AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
- () => applyNewResult(judgementResult));
+ TaikoMascotAnimationState[] mascotStates = null;
- AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState));
+ AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
+ () =>
+ {
+ applyNewResult(judgementResult);
+ // store the states as soon as possible, so that the delay between steps doesn't incorrectly fail the test
+ // due to not checking if the state changed quickly enough.
+ Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
+ });
+
+ AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState));
}
private void applyNewResult(JudgementResult judgementResult)
@@ -211,6 +224,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}
private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state);
- private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state);
+ private bool animatedMascotsIn(TaikoMascotAnimationState state) => animatedMascots.Any(d => d.State.Value == state);
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
index f7729138ff..d0c57b20c0 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestCase("basic")]
[TestCase("slider-generating-drumroll")]
[TestCase("sample-to-type-conversions")]
+ [TestCase("slider-conversion-v6")]
+ [TestCase("slider-conversion-v14")]
public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
new file mode 100644
index 0000000000..7089ea6619
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Reflection;
+using NUnit.Framework;
+using osu.Framework.IO.Stores;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneTaikoHitObjectSamples : HitObjectSampleTest
+ {
+ protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
+
+ protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples)));
+
+ [TestCase("taiko-normal-hitnormal")]
+ [TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
+ public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
+ {
+ SetupSkins(expectedSample, expectedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertBeatmapLookup(expectedSample);
+ }
+
+ [TestCase("taiko-normal-hitnormal")]
+ [TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
+ public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
+ {
+ SetupSkins(string.Empty, expectedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertUserLookup(expectedSample);
+ }
+
+ [TestCase("taiko-normal-hitnormal2")]
+ [TestCase("normal-hitnormal2")]
+ public void TestUserSkinLookupIgnoresSampleBank(string unwantedSample)
+ {
+ SetupSkins(string.Empty, unwantedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertNoLookup(unwantedSample);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
index aaa634648a..0be005e1c4 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
};
[Test]
- public void TestSpinnerDoesNotFail()
+ public void TestSpinnerDoesFail()
{
bool judged = false;
AddStep("Setup judgements", () =>
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Player.ScoreProcessor.NewJudgement += b => judged = true;
});
AddUntilStep("swell judged", () => judged);
- AddAssert("not failed", () => !Player.HasFailed);
+ AddAssert("failed", () => Player.HasFailed);
}
}
}
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 28b8476a22..ada7ac5d74 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
new file mode 100644
index 0000000000..7c39c040b1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.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.Collections.Generic;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Audio;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Taiko.Audio
+{
+ ///
+ /// Stores samples for the input drum.
+ /// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
+ ///
+ public class DrumSampleContainer : LifetimeManagementContainer
+ {
+ private readonly ControlPointInfo controlPoints;
+ private readonly Dictionary mappings = new Dictionary();
+
+ public DrumSampleContainer(ControlPointInfo controlPoints)
+ {
+ this.controlPoints = controlPoints;
+
+ IReadOnlyList samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
+
+ for (int i = 0; i < samplePoints.Count; i++)
+ {
+ var samplePoint = samplePoints[i];
+
+ var centre = samplePoint.GetSampleInfo();
+ var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP);
+
+ var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
+ var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue;
+
+ mappings[samplePoint.Time] = new DrumSample
+ {
+ Centre = addSound(centre, lifetimeStart, lifetimeEnd),
+ Rim = addSound(rim, lifetimeStart, lifetimeEnd)
+ };
+ }
+ }
+
+ private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
+ {
+ var drawable = new SkinnableSound(hitSampleInfo)
+ {
+ LifetimeStart = lifetimeStart,
+ LifetimeEnd = lifetimeEnd
+ };
+ AddInternal(drawable);
+ return drawable;
+ }
+
+ public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
+
+ public class DrumSample
+ {
+ public SkinnableSound Centre;
+ public SkinnableSound Rim;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
deleted file mode 100644
index c31b07344d..0000000000
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.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.
-
-using System.Collections.Generic;
-using osu.Game.Audio;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Skinning;
-
-namespace osu.Game.Rulesets.Taiko.Audio
-{
- public class DrumSampleMapping
- {
- private readonly ControlPointInfo controlPoints;
- private readonly Dictionary mappings = new Dictionary();
-
- public readonly List Sounds = new List();
-
- public DrumSampleMapping(ControlPointInfo controlPoints)
- {
- this.controlPoints = controlPoints;
-
- IEnumerable samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
-
- foreach (var s in samplePoints)
- {
- var centre = s.GetSampleInfo();
- var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP);
-
- mappings[s.Time] = new DrumSample
- {
- Centre = addSound(centre),
- Rim = addSound(rim)
- };
- }
- }
-
- private SkinnableSound addSound(HitSampleInfo hitSampleInfo)
- {
- var drawable = new SkinnableSound(hitSampleInfo);
- Sounds.Add(drawable);
- return drawable;
- }
-
- public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
-
- public class DrumSample
- {
- public SkinnableSound Centre;
- public SkinnableSound Rim;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 78550ed270..2a1aa5d1df 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Formats;
namespace osu.Game.Rulesets.Taiko.Beatmaps
{
@@ -82,37 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
case IHasDistance distanceData:
{
- // Number of spans of the object - one for the initial length and for each repeat
- int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
-
- TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
- DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
-
- double speedAdjustment = difficultyPoint.SpeedMultiplier;
- double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
-
- // The true distance, accounting for any repeats. This ends up being the drum roll distance later
- double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
-
- // The velocity of the taiko hit object - calculated as the velocity of a drum roll
- double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
- // The duration of the taiko hit object
- double taikoDuration = distance / taikoVelocity;
-
- // The velocity of the osu! hit object - calculated as the velocity of a slider
- double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
- // The duration of the osu! hit object
- double osuDuration = distance / osuVelocity;
-
- // osu-stable always uses the speed-adjusted beatlength to determine the velocities, but
- // only uses it for tick rate if beatmap version < 8
- if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
- speedAdjustedBeatLength *= speedAdjustment;
-
- // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
- double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
-
- if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
+ if (shouldConvertSliderToHits(obj, beatmap, distanceData, out var taikoDuration, out var tickSpacing))
{
List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples });
@@ -184,6 +155,52 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}
}
+ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out double taikoDuration, out double tickSpacing)
+ {
+ // DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS.
+ // Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable.
+ // Rounding cannot be used as an alternative since the error deltas have been observed to be between 1e-2 and 1e-6.
+
+ // The true distance, accounting for any repeats. This ends up being the drum roll distance later
+ int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
+ double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
+
+ TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
+ DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
+
+ double beatLength;
+#pragma warning disable 618
+ if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
+#pragma warning restore 618
+ beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
+ else
+ beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
+
+ double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
+
+ // The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
+ double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
+ taikoDuration = distance / taikoVelocity * beatLength;
+
+ if (isForCurrentRuleset)
+ {
+ tickSpacing = 0;
+ return false;
+ }
+
+ double osuVelocity = taikoVelocity * (1000f / beatLength);
+
+ // osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8
+ if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
+ beatLength = timingPoint.BeatLength;
+
+ // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
+ tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
+
+ return tickSpacing > 0
+ && distance / osuVelocity * 1000 < 2 * beatLength;
+ }
+
protected override Beatmap CreateBeatmap() => new TaikoBeatmap();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
index c6fe273b50..2c1885ae1a 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
double addition = 1;
// We get an extra addition if we are not a slider or spinner
- if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)
+ if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000)
{
if (hasColourChange(current))
addition += 0.75;
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
index 604daa929f..0d91002f4b 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoDrumRollJudgement : TaikoJudgement
{
- public override bool AffectsCombo => false;
-
protected override double HealthIncreaseFor(HitResult result)
{
// Drum rolls can be ignored with no health penalty
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
index 29be5e0eac..4d61efd3ee 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoSwellJudgement : TaikoJudgement
{
- public override bool AffectsCombo => false;
-
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs
index 97337acc45..138e8f9785 100644
--- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Replays
protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any();
- public override List GetPendingInputs() => new List { new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() } };
+ public override void CollectPendingInputs(List inputs)
+ {
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json
new file mode 100644
index 0000000000..6a6063cb74
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json
@@ -0,0 +1,379 @@
+{
+ "Mappings": [{
+ "StartTime": 2000,
+ "Objects": [{
+ "StartTime": 2000,
+ "EndTime": 2000,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 2173,
+ "EndTime": 2173,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ },
+ {
+ "StartTime": 4000,
+ "Objects": [{
+ "StartTime": 4000,
+ "EndTime": 4000,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 4173,
+ "EndTime": 4173,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ },
+ {
+ "StartTime": 6000,
+ "Objects": [{
+ "StartTime": 6000,
+ "EndTime": 6000,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 6271,
+ "EndTime": 6271,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 6542,
+ "EndTime": 6542,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ },
+ {
+ "StartTime": 8000,
+ "Objects": [{
+ "StartTime": 8000,
+ "EndTime": 8000,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8026,
+ "EndTime": 8026,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8053,
+ "EndTime": 8053,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8080,
+ "EndTime": 8080,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8107,
+ "EndTime": 8107,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8133,
+ "EndTime": 8133,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8160,
+ "EndTime": 8160,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8187,
+ "EndTime": 8187,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8214,
+ "EndTime": 8214,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8241,
+ "EndTime": 8241,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8267,
+ "EndTime": 8267,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8294,
+ "EndTime": 8294,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8321,
+ "EndTime": 8321,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8348,
+ "EndTime": 8348,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8374,
+ "EndTime": 8374,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8401,
+ "EndTime": 8401,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8428,
+ "EndTime": 8428,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8455,
+ "EndTime": 8455,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8482,
+ "EndTime": 8482,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8508,
+ "EndTime": 8508,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8535,
+ "EndTime": 8535,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8562,
+ "EndTime": 8562,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8589,
+ "EndTime": 8589,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8615,
+ "EndTime": 8615,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8642,
+ "EndTime": 8642,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8669,
+ "EndTime": 8669,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8696,
+ "EndTime": 8696,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8723,
+ "EndTime": 8723,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8749,
+ "EndTime": 8749,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8776,
+ "EndTime": 8776,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8803,
+ "EndTime": 8803,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8830,
+ "EndTime": 8830,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8857,
+ "EndTime": 8857,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu
new file mode 100644
index 0000000000..4c8fb1fde6
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu
@@ -0,0 +1,32 @@
+osu file format v14
+
+[General]
+Mode: 0
+
+[Difficulty]
+HPDrainRate:7
+CircleSize:4
+OverallDifficulty:8
+ApproachRate:9.2
+SliderMultiplier:2.3
+SliderTickRate:1
+
+[TimingPoints]
+0,333.333333333333,4,1,0,50,1,0
+2000,-100,4,2,0,80,0,0
+
+6000,389.61038961039,4,2,1,60,1,0
+
+8000,428.571428571429,4,3,1,65,1,0
+8000,-133.333333333333,4,1,1,45,0,0
+
+[HitObjects]
+// Should convert.
+48,32,2000,6,0,B|168:32,1,120,4|2
+312,68,4000,2,0,B|288:52|256:44|216:52|200:68,1,120,0|8
+
+// Should convert.
+184,224,6000,2,0,L|336:308,2,160,2|2|0,0:0|0:0|0:0,0:0:0:0:
+
+// Should convert.
+328,36,8000,6,0,L|332:16,32,10.7812504112721,0|0,0:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json
new file mode 100644
index 0000000000..c3d3c52ebd
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json
@@ -0,0 +1,137 @@
+{
+ "Mappings": [{
+ "StartTime": 0,
+ "Objects": [{
+ "StartTime": 0,
+ "EndTime": 0,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 162,
+ "EndTime": 162,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 325,
+ "EndTime": 325,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 487,
+ "EndTime": 487,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 650,
+ "EndTime": 650,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 813,
+ "EndTime": 813,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 975,
+ "EndTime": 975,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ }
+ ]
+ },
+ {
+ "StartTime": 2000,
+ "Objects": [{
+ "StartTime": 2000,
+ "EndTime": 2000,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 2162,
+ "EndTime": 2162,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 2325,
+ "EndTime": 2325,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 2487,
+ "EndTime": 2487,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 2650,
+ "EndTime": 2650,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 2813,
+ "EndTime": 2813,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 2975,
+ "EndTime": 2975,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu
new file mode 100644
index 0000000000..c1e4c3bbd7
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu
@@ -0,0 +1,20 @@
+osu file format v6
+
+[General]
+Mode: 0
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:4
+OverallDifficulty:1
+SliderMultiplier:1.2
+SliderTickRate:3
+
+[TimingPoints]
+0,487.884208814441,4,1,0,60,1,0
+2000,-100,4,1,0,65,0,1
+
+[HitObjects]
+// Should convert.
+376,64,0,6,0,B|256:32|136:64,1,240,6|0
+256,120,2000,6,8,C|264:192|336:192,2,120,8|8|6
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
index 003d40af56..e29ea87d25 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
@@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
internal class TaikoScoreProcessor : ScoreProcessor
{
+ protected override double DefaultAccuracyPortion => 0.75;
+
+ protected override double DefaultComboPortion => 0.25;
+
public override HitWindows CreateHitWindows() => new TaikoHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
index 8531f3cefd..8223e3bc01 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Skinning;
@@ -34,13 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
- Texture = skin.GetTexture("taiko-roll-end"),
+ Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
FillMode = FillMode.Fit,
},
body = new Sprite
{
RelativeSizeAxes = Axes.Both,
- Texture = skin.GetTexture("taiko-roll-middle"),
+ Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
},
headCircle = new LegacyCirclePiece
{
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
index 81d645e294..b7b55b9ae0 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
public readonly Sprite Centre;
[Resolved]
- private DrumSampleMapping sampleMappings { get; set; }
+ private DrumSampleContainer sampleContainer { get; set; }
public LegacyHalfDrum(bool flipped)
{
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
public bool OnPressed(TaikoAction action)
{
Drawable target = null;
- var drumSample = sampleMappings.SampleAt(Time.Current);
+ var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction)
{
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index 23d675cfb0..f032c5f485 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -91,10 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null;
case TaikoSkinComponents.Mascot:
- if (GetTexture("pippidonclear0") != null)
- return new DrawableTaikoMascot();
-
- return null;
+ return new DrawableTaikoMascot();
}
return Source.GetDrawableComponent(component);
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 4cdd1fbc24..2011842591 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -21,13 +21,18 @@ using osu.Game.Rulesets.Taiko.Difficulty;
using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Scoring;
using System;
+using System.Linq;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit;
+using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Skinning;
+using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko
{
+ [ExcludeFromDynamicCompile]
public class TaikoRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
@@ -155,5 +160,20 @@ namespace osu.Game.Rulesets.Taiko
public int LegacyID => 1;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
+
+ public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ {
+ new StatisticRow
+ {
+ Columns = new[]
+ {
+ new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList())
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }),
+ }
+ }
+ };
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 06ccd45cb8..5966b24b34 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float middle_split = 0.025f;
[Cached]
- private DrumSampleMapping sampleMapping;
+ private DrumSampleContainer sampleContainer;
public InputDrum(ControlPointInfo controlPoints)
{
- sampleMapping = new DrumSampleMapping(controlPoints);
+ sampleContainer = new DrumSampleContainer(controlPoints);
RelativeSizeAxes = Axes.Both;
}
@@ -37,39 +37,41 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
- Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
- Scale = new Vector2(0.9f),
- Children = new Drawable[]
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
{
- new TaikoHalfDrum(false)
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Scale = new Vector2(0.9f),
+ Children = new Drawable[]
{
- Name = "Left Half",
- Anchor = Anchor.Centre,
- Origin = Anchor.CentreRight,
- RelativeSizeAxes = Axes.Both,
- RelativePositionAxes = Axes.X,
- X = -middle_split / 2,
- RimAction = TaikoAction.LeftRim,
- CentreAction = TaikoAction.LeftCentre
- },
- new TaikoHalfDrum(true)
- {
- Name = "Right Half",
- Anchor = Anchor.Centre,
- Origin = Anchor.CentreLeft,
- RelativeSizeAxes = Axes.Both,
- RelativePositionAxes = Axes.X,
- X = middle_split / 2,
- RimAction = TaikoAction.RightRim,
- CentreAction = TaikoAction.RightCentre
+ new TaikoHalfDrum(false)
+ {
+ Name = "Left Half",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ X = -middle_split / 2,
+ RimAction = TaikoAction.LeftRim,
+ CentreAction = TaikoAction.LeftCentre
+ },
+ new TaikoHalfDrum(true)
+ {
+ Name = "Right Half",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ X = middle_split / 2,
+ RimAction = TaikoAction.RightRim,
+ CentreAction = TaikoAction.RightCentre
+ }
}
- }
- });
-
- AddRangeInternal(sampleMapping.Sounds);
+ }),
+ sampleContainer
+ };
}
///
@@ -93,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centreHit;
[Resolved]
- private DrumSampleMapping sampleMappings { get; set; }
+ private DrumSampleContainer sampleContainer { get; set; }
public TaikoHalfDrum(bool flipped)
{
@@ -154,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Drawable target = null;
Drawable back = null;
- var drumSample = sampleMappings.SampleAt(Time.Current);
+ var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction)
{
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
index 7de1593ab6..caddc8b122 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
@@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
+ Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
Masking = true,
BorderColour = Color4.White,
BorderThickness = border_thickness,
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE),
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE),
Masking = true,
BorderColour = Color4.White,
BorderThickness = border_thickness,
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
index 6f25a5f662..9c76aea54c 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
@@ -128,6 +128,13 @@ namespace osu.Game.Rulesets.Taiko.UI
}
private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
- => skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
+ {
+ var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
+
+ if (frameIndex == 0 && texture == null)
+ texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}");
+
+ return texture;
+ }
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
index e50b2231bf..460ad1b898 100644
--- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs
@@ -157,6 +157,24 @@ namespace osu.Game.Tests.Gameplay
assertHealthNotEqualTo(1);
}
+ [Test]
+ public void TestBonusObjectsExcludedFromDrain()
+ {
+ var beatmap = new Beatmap
+ {
+ BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } },
+ };
+
+ beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 });
+ for (double time = 0; time < 5000; time += 100)
+ beatmap.HitObjects.Add(new JudgeableHitObject(false) { StartTime = time });
+ beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 5000 });
+
+ createProcessor(beatmap);
+ setTime(4900); // Get close to the second combo-affecting object
+ assertHealthNotEqualTo(0);
+ }
+
private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks)
{
var beatmap = new Beatmap
@@ -197,8 +215,25 @@ namespace osu.Game.Tests.Gameplay
private class JudgeableHitObject : HitObject
{
- public override Judgement CreateJudgement() => new Judgement();
+ private readonly bool affectsCombo;
+
+ public JudgeableHitObject(bool affectsCombo = true)
+ {
+ this.affectsCombo = affectsCombo;
+ }
+
+ public override Judgement CreateJudgement() => new TestJudgement(affectsCombo);
protected override HitWindows CreateHitWindows() => new HitWindows();
+
+ private class TestJudgement : Judgement
+ {
+ public override bool AffectsCombo { get; }
+
+ public TestJudgement(bool affectsCombo)
+ {
+ AffectsCombo = affectsCombo;
+ }
+ }
}
}
}
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
index 7a89642e11..2d5e4b911e 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
@@ -118,7 +119,7 @@ namespace osu.Game.Tests.Gameplay
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
- public Texture GetTexture(string componentName) => throw new NotImplementedException();
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
index 737946e1e0..6b95931b21 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
@@ -6,9 +6,9 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
-using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
+using static osu.Game.Skinning.LegacySkinConfiguration;
namespace osu.Game.Tests.Gameplay
{
@@ -67,9 +67,11 @@ namespace osu.Game.Tests.Gameplay
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
/// normal-hitnormal2
/// normal-hitnormal
+ /// hitnormal
///
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{
SetupSkins(expectedSample, expectedSample);
@@ -80,12 +82,13 @@ namespace osu.Game.Tests.Gameplay
}
///
- /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample:
- /// normal-hitnormal2
+ /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin
+ /// (ignoring the custom sample set index) when the beatmap skin does not contain the sample:
/// normal-hitnormal
+ /// hitnormal
///
- [TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
{
SetupSkins(string.Empty, expectedSample);
@@ -95,6 +98,23 @@ namespace osu.Game.Tests.Gameplay
AssertUserLookup(expectedSample);
}
+ ///
+ /// Tests that a hitobject which provides a custom sample set of 2 does not retrieve a normal-hitnormal2 sample from the user skin
+ /// if the beatmap skin does not contain the sample.
+ /// User skins in stable ignore the custom sample set index when performing lookups.
+ ///
+ [Test]
+ public void TestUserSkinLookupIgnoresSampleBank()
+ {
+ const string unwanted_sample = "normal-hitnormal2";
+
+ SetupSkins(string.Empty, unwanted_sample);
+
+ CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+
+ AssertNoLookup(unwanted_sample);
+ }
+
///
/// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
///
@@ -145,6 +165,7 @@ namespace osu.Game.Tests.Gameplay
///
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestControlPointCustomSampleFromBeatmap(string sampleName)
{
SetupSkins(sampleName, sampleName);
@@ -178,7 +199,7 @@ namespace osu.Game.Tests.Gameplay
string[] expectedSamples =
{
"normal-hitnormal2",
- "normal-hitwhistle2"
+ "normal-hitwhistle" // user skin lookups ignore custom sample set index
};
SetupSkins(expectedSamples[0], expectedSamples[1]);
@@ -190,7 +211,7 @@ namespace osu.Game.Tests.Gameplay
}
///
- /// Tests that when a custom sample bank is used, but is disabled,
+ /// Tests that when a custom sample bank is used, but is disabled,
/// only the additional sound will be looked up.
///
[Test]
@@ -209,7 +230,7 @@ namespace osu.Game.Tests.Gameplay
}
///
- /// Tests that when a normal sample bank is used and is disabled,
+ /// Tests that when a normal sample bank is used and is disabled,
/// the normal sound will be looked up anyway.
///
[Test]
@@ -226,6 +247,6 @@ namespace osu.Game.Tests.Gameplay
}
private void disableLayeredHitSounds()
- => AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0");
+ => AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[LegacySetting.LayeredHitSounds.ToString()] = "0");
}
}
diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
index 43c1c77786..6e83c78fda 100644
--- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
+++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs
@@ -19,24 +19,18 @@ namespace osu.Game.Tests.NonVisual
[TestFixture]
public class CustomDataDirectoryTest
{
- [SetUp]
- public void SetUp()
- {
- if (Directory.Exists(customPath))
- Directory.Delete(customPath, true);
- }
-
[Test]
public void TestDefaultDirectory()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestDefaultDirectory)))
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestDefaultDirectory)))
{
try
{
+ string defaultStorageLocation = getDefaultLocationFor(nameof(TestDefaultDirectory));
+
var osu = loadOsu(host);
var storage = osu.Dependencies.Get();
- string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestDefaultDirectory));
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
}
finally
@@ -46,21 +40,14 @@ namespace osu.Game.Tests.NonVisual
}
}
- private string customPath => Path.Combine(RuntimeInfo.StartupDirectory, "custom-path");
-
[Test]
public void TestCustomDirectory()
{
- using (var host = new HeadlessGameHost(nameof(TestCustomDirectory)))
+ string customPath = prepareCustomPath();
+
+ using (var host = new CustomTestHeadlessGameHost(nameof(TestCustomDirectory)))
{
- string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestCustomDirectory));
-
- // need access before the game has constructed its own storage yet.
- Storage storage = new DesktopStorage(defaultStorageLocation, host);
- // manual cleaning so we can prepare a config file.
- storage.DeleteDirectory(string.Empty);
-
- using (var storageConfig = new StorageConfigManager(storage))
+ using (var storageConfig = new StorageConfigManager(host.InitialStorage))
storageConfig.Set(StorageConfig.FullPath, customPath);
try
@@ -68,7 +55,7 @@ namespace osu.Game.Tests.NonVisual
var osu = loadOsu(host);
// switch to DI'd storage
- storage = osu.Dependencies.Get();
+ var storage = osu.Dependencies.Get();
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
}
@@ -82,16 +69,11 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestSubDirectoryLookup()
{
- using (var host = new HeadlessGameHost(nameof(TestSubDirectoryLookup)))
+ string customPath = prepareCustomPath();
+
+ using (var host = new CustomTestHeadlessGameHost(nameof(TestSubDirectoryLookup)))
{
- string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestSubDirectoryLookup));
-
- // need access before the game has constructed its own storage yet.
- Storage storage = new DesktopStorage(defaultStorageLocation, host);
- // manual cleaning so we can prepare a config file.
- storage.DeleteDirectory(string.Empty);
-
- using (var storageConfig = new StorageConfigManager(storage))
+ using (var storageConfig = new StorageConfigManager(host.InitialStorage))
storageConfig.Set(StorageConfig.FullPath, customPath);
try
@@ -99,7 +81,7 @@ namespace osu.Game.Tests.NonVisual
var osu = loadOsu(host);
// switch to DI'd storage
- storage = osu.Dependencies.Get();
+ var storage = osu.Dependencies.Get();
string actualTestFile = Path.Combine(customPath, "rulesets", "test");
@@ -120,14 +102,21 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigration()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigration)))
+ string customPath = prepareCustomPath();
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigration)))
{
try
{
+ string defaultStorageLocation = getDefaultLocationFor(nameof(TestMigration));
+
var osu = loadOsu(host);
var storage = osu.Dependencies.Get();
var osuStorage = storage as MigratableStorage;
+ // Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
+ string originalDirectory = storage.GetFullPath(".");
+
// ensure we perform a save
host.Dependencies.Get().Save();
@@ -137,8 +126,6 @@ namespace osu.Game.Tests.NonVisual
// for testing nested files are not ignored (only top level)
host.Storage.GetStorageForDirectory("test-nested").GetStorageForDirectory("cache");
- string defaultStorageLocation = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration));
-
Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation));
osu.Migrate(customPath);
@@ -146,27 +133,27 @@ namespace osu.Game.Tests.NonVisual
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
// ensure cache was not moved
- Assert.That(host.Storage.ExistsDirectory("cache"));
+ Assert.That(Directory.Exists(Path.Combine(originalDirectory, "cache")));
// ensure nested cache was moved
- Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
+ Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
Assert.That(osuStorage, Is.Not.Null);
foreach (var file in osuStorage.IgnoreFiles)
{
- Assert.That(host.Storage.Exists(file), Is.True);
+ Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False);
}
foreach (var dir in osuStorage.IgnoreDirectories)
{
- Assert.That(host.Storage.ExistsDirectory(dir), Is.True);
+ Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.ExistsDirectory(dir), Is.False);
}
- Assert.That(new StreamReader(host.Storage.GetStream("storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
+ Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
}
finally
{
@@ -178,14 +165,15 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationBetweenTwoTargets()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets)))
+ string customPath = prepareCustomPath();
+ string customPath2 = prepareCustomPath("-2");
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets)))
{
try
{
var osu = loadOsu(host);
- string customPath2 = $"{customPath}-2";
-
const string database_filename = "client.db";
Assert.DoesNotThrow(() => osu.Migrate(customPath));
@@ -207,7 +195,9 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToSameTargetFails()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSameTargetFails)))
+ string customPath = prepareCustomPath();
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSameTargetFails)))
{
try
{
@@ -226,7 +216,9 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToNestedTargetFails()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToNestedTargetFails)))
+ string customPath = prepareCustomPath();
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToNestedTargetFails)))
{
try
{
@@ -253,7 +245,9 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestMigrationToSeeminglyNestedTarget()
{
- using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget)))
+ string customPath = prepareCustomPath();
+
+ using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget)))
{
try
{
@@ -282,6 +276,7 @@ namespace osu.Game.Tests.NonVisual
var osu = new OsuGameBase();
Task.Run(() => host.Run(osu));
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
+
return osu;
}
@@ -294,5 +289,39 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
+
+ private static string getDefaultLocationFor(string testTypeName)
+ {
+ string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName);
+
+ if (Directory.Exists(path))
+ Directory.Delete(path, true);
+
+ return path;
+ }
+
+ private string prepareCustomPath(string suffix = "")
+ {
+ string path = Path.Combine(RuntimeInfo.StartupDirectory, $"custom-path{suffix}");
+
+ if (Directory.Exists(path))
+ Directory.Delete(path, true);
+
+ return path;
+ }
+
+ public class CustomTestHeadlessGameHost : HeadlessGameHost
+ {
+ public Storage InitialStorage { get; }
+
+ public CustomTestHeadlessGameHost(string name)
+ : base(name)
+ {
+ string defaultStorageLocation = getDefaultLocationFor(name);
+
+ InitialStorage = new DesktopStorage(defaultStorageLocation, this);
+ InitialStorage.DeleteDirectory(string.Empty);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Online/TestAPIModSerialization.cs b/osu.Game.Tests/Online/TestAPIModSerialization.cs
index d9318aa822..5948582d77 100644
--- a/osu.Game.Tests/Online/TestAPIModSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModSerialization.cs
@@ -49,9 +49,32 @@ namespace osu.Game.Tests.Online
Assert.That(converted.TestSetting.Value, Is.EqualTo(2));
}
+ [Test]
+ public void TestDeserialiseTimeRampMod()
+ {
+ // Create the mod with values different from default.
+ var apiMod = new APIMod(new TestModTimeRamp
+ {
+ AdjustPitch = { Value = false },
+ InitialRate = { Value = 1.25 },
+ FinalRate = { Value = 0.25 }
+ });
+
+ var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod));
+ var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset());
+
+ Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false));
+ Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25));
+ Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
+ }
+
private class TestRuleset : Ruleset
{
- public override IEnumerable GetModsFor(ModType type) => new[] { new TestMod() };
+ public override IEnumerable GetModsFor(ModType type) => new Mod[]
+ {
+ new TestMod(),
+ new TestModTimeRamp(),
+ };
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException();
@@ -78,5 +101,39 @@ namespace osu.Game.Tests.Online
Precision = 0.01,
};
}
+
+ private class TestModTimeRamp : ModTimeRamp
+ {
+ public override string Name => "Test Mod";
+ public override string Acronym => "TMTR";
+ public override double ScoreMultiplier => 1;
+
+ [SettingSource("Initial rate", "The starting speed of the track")]
+ public override BindableNumber InitialRate { get; } = new BindableDouble
+ {
+ MinValue = 1,
+ MaxValue = 2,
+ Default = 1.5,
+ Value = 1.5,
+ Precision = 0.01,
+ };
+
+ [SettingSource("Final rate", "The speed increase to ramp towards")]
+ public override BindableNumber FinalRate { get; } = new BindableDouble
+ {
+ MinValue = 0,
+ MaxValue = 1,
+ Default = 0.5,
+ Value = 0.5,
+ Precision = 0.01,
+ };
+
+ [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
+ public override BindableBool AdjustPitch { get; } = new BindableBool
+ {
+ Default = true,
+ Value = true
+ };
+ }
}
}
diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
index 1e77d50115..42948c3731 100644
--- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
+++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
@@ -8,7 +8,6 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Tests.Visual;
-using osu.Game.Users;
namespace osu.Game.Tests.Online
{
@@ -55,7 +54,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () =>
{
gotResponse = false;
- request = new LeaveChannelRequest(new Channel(), new User());
+ request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true;
API.Queue(request);
});
@@ -74,7 +73,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () =>
{
gotResponse = false;
- request = new LeaveChannelRequest(new Channel(), new User());
+ request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true;
API.Perform(request);
});
@@ -93,7 +92,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () =>
{
gotResponse = false;
- request = new LeaveChannelRequest(new Channel(), new User());
+ request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true;
API.PerformAsync(request);
});
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index 8deed75a56..ad5b3ec0f6 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
@@ -216,7 +217,7 @@ namespace osu.Game.Tests.Skins
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
- public Texture GetTexture(string componentName) => skin.GetTexture(componentName);
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT);
public SampleChannel GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
index 41d1459103..3a19eabe81 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
@@ -175,13 +175,13 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("Seek(49.999)", () => Clock.Seek(49.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
- AddAssert("Time = 50", () => Clock.CurrentTime == 50);
+ AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(99)", () => Clock.Seek(99));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(99.999)", () => Clock.Seek(99.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
- AddAssert("Time = 100", () => Clock.CurrentTime == 100);
+ AddAssert("Time = 100", () => Clock.CurrentTime == 150);
AddStep("Seek(174)", () => Clock.Seek(174));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
index 2a7f9389d1..09f5ac2224 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
@@ -13,6 +14,7 @@ namespace osu.Game.Tests.Visual.Editing
public class TestSceneTimingScreen : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
+ [Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
public TestSceneTimingScreen()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 0be949650e..4743317fdd 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -35,6 +36,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
+ double? time = null;
+
+ AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
+
+ // test seek via keyboard
+ AddStep("seek with right arrow key", () => press(Key.Right));
+ AddAssert("time seeked forward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime > time + 2000);
+
+ AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
+ AddStep("seek with left arrow key", () => press(Key.Left));
+ AddAssert("time seeked backward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < time);
+
seekToBreak(0);
seekToBreak(1);
@@ -54,5 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay
BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex);
}
+
+ private void press(Key key)
+ {
+ InputManager.PressKey(key);
+ InputManager.ReleaseKey(key);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
index a95e806862..1c55595c97 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailingLayer.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
@@ -14,6 +15,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private FailingLayer layer;
+ private readonly Bindable showHealth = new Bindable();
+
[Resolved]
private OsuConfigManager config { get; set; }
@@ -24,8 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
Child = layer = new FailingLayer();
layer.BindHealthProcessor(new DrainingHealthProcessor(1));
+ layer.ShowHealth.BindTo(showHealth);
});
+ AddStep("show health", () => showHealth.Value = true);
AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
AddUntilStep("layer is visible", () => layer.IsPresent);
}
@@ -69,5 +74,27 @@ namespace osu.Game.Tests.Visual.Gameplay
AddWaitStep("wait for potential fade", 10);
AddAssert("layer is still visible", () => layer.IsPresent);
}
+
+ [Test]
+ public void TestLayerVisibilityWithDifferentOptions()
+ {
+ AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
+
+ AddStep("don't show health", () => showHealth.Value = false);
+ AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+ AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
+
+ AddStep("don't show health", () => showHealth.Value = false);
+ AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+ AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
+
+ AddStep("show health", () => showHealth.Value = true);
+ AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
+ AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
+
+ AddStep("show health", () => showHealth.Value = true);
+ AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
+ AddUntilStep("layer fade is visible", () => layer.IsPresent);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 1961a224c1..420bf29429 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -11,6 +11,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets;
using osu.Game.Screens.Play;
+using osu.Game.Skinning;
using osuTK;
using osuTK.Input;
@@ -221,6 +222,31 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmExited();
}
+ [Test]
+ public void TestPauseSoundLoop()
+ {
+ AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
+
+ SkinnableSound getLoop() => Player.ChildrenOfType().FirstOrDefault()?.ChildrenOfType().FirstOrDefault();
+
+ pauseAndConfirm();
+ AddAssert("loop is playing", () => getLoop().IsPlaying);
+
+ resumeAndConfirm();
+ AddUntilStep("loop is stopped", () => !getLoop().IsPlaying);
+
+ AddUntilStep("pause again", () =>
+ {
+ Player.Pause();
+ return !Player.GameplayClockContainer.GameplayClock.IsRunning;
+ });
+
+ AddAssert("loop is playing", () => getLoop().IsPlaying);
+
+ resumeAndConfirm();
+ AddUntilStep("loop is stopped", () => !getLoop().IsPlaying);
+ }
+
private void pauseAndConfirm()
{
pause();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index c7455583e4..bc1c10e59d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -173,19 +173,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- return new List
- {
- new MousePositionAbsoluteInput
- {
- Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
- },
- new ReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List()
- }
- };
+ inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
index 7822f07957..c0f99db85d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
@@ -113,19 +113,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- return new List
- {
- new MousePositionAbsoluteInput
- {
- Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
- },
- new ReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List()
- }
- };
+ inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index 3b91243fee..bed48f3d86 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
@@ -295,7 +296,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
: null;
- public Texture GetTexture(string componentName) => throw new NotImplementedException();
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
@@ -306,7 +307,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public Drawable GetDrawableComponent(ISkinComponent componentName) => new SecondarySourceBox();
- public Texture GetTexture(string componentName) => throw new NotImplementedException();
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
@@ -318,7 +319,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public Drawable GetDrawableComponent(ISkinComponent componentName) => new BaseSourceBox();
- public Texture GetTexture(string componentName) => throw new NotImplementedException();
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
new file mode 100644
index 0000000000..e0a1f947ec
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -0,0 +1,105 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Audio;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Audio;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSkinnableSound : OsuTestScene
+ {
+ [Cached]
+ private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
+
+ private SkinnableSound skinnableSound;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ gameplayClock.IsPaused.Value = false;
+
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ Clock = gameplayClock,
+ RelativeSizeAxes = Axes.Both,
+ Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide"))
+ },
+ };
+ });
+
+ [Test]
+ public void TestStoppedSoundDoesntResumeAfterPause()
+ {
+ DrawableSample sample = null;
+ AddStep("start sample with looping", () =>
+ {
+ sample = skinnableSound.ChildrenOfType().First();
+
+ skinnableSound.Looping = true;
+ skinnableSound.Play();
+ });
+
+ AddUntilStep("wait for sample to start playing", () => sample.Playing);
+
+ AddStep("stop sample", () => skinnableSound.Stop());
+
+ AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+
+ AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
+
+ AddWaitStep("wait a bit", 5);
+ AddAssert("sample not playing", () => !sample.Playing);
+ }
+
+ [Test]
+ public void TestLoopingSoundResumesAfterPause()
+ {
+ DrawableSample sample = null;
+ AddStep("start sample with looping", () =>
+ {
+ skinnableSound.Looping = true;
+ skinnableSound.Play();
+ sample = skinnableSound.ChildrenOfType().First();
+ });
+
+ AddUntilStep("wait for sample to start playing", () => sample.Playing);
+
+ AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+ }
+
+ [Test]
+ public void TestNonLoopingStopsWithPause()
+ {
+ DrawableSample sample = null;
+ AddStep("start sample", () =>
+ {
+ skinnableSound.Play();
+ sample = skinnableSound.ChildrenOfType().First();
+ });
+
+ AddAssert("sample playing", () => sample.Playing);
+
+ AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+
+ AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
+
+ AddAssert("sample not playing", () => !sample.Playing);
+ AddAssert("sample not playing", () => !sample.Playing);
+ AddAssert("sample not playing", () => !sample.Playing);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
index 2d2f1a1618..f71d13ed35 100644
--- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
+++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
@@ -19,10 +19,10 @@ namespace osu.Game.Tests.Visual.Menus
[Cached]
private OsuLogo logo;
+ protected OsuScreenStack IntroStack;
+
protected IntroTestScene()
{
- OsuScreenStack introStack = null;
-
Children = new Drawable[]
{
new Box
@@ -45,17 +45,17 @@ namespace osu.Game.Tests.Visual.Menus
logo.FinishTransforms();
logo.IsTracking = false;
- introStack?.Expire();
+ IntroStack?.Expire();
- Add(introStack = new OsuScreenStack
+ Add(IntroStack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
});
- introStack.Push(CreateScreen());
+ IntroStack.Push(CreateScreen());
});
- AddUntilStep("wait for menu", () => introStack.CurrentScreen is MainMenu);
+ AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu);
}
protected abstract IScreen CreateScreen();
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
index 905f17ef0b..8f20e38494 100644
--- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
+++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Framework.Audio.Track;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
@@ -11,5 +12,14 @@ namespace osu.Game.Tests.Visual.Menus
public class TestSceneIntroWelcome : IntroTestScene
{
protected override IScreen CreateScreen() => new IntroWelcome();
+
+ public TestSceneIntroWelcome()
+ {
+ AddUntilStep("wait for load", () => getTrack() != null);
+
+ AddAssert("check if menu music loops", () => getTrack().Looping);
+ }
+
+ private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track;
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs
new file mode 100644
index 0000000000..8b7e0fd9da
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Game.Beatmaps;
+using osu.Game.Online.Multiplayer;
+using osu.Game.Rulesets;
+using osu.Game.Screens.Multi;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public abstract class RoomManagerTestScene : MultiplayerTestScene
+ {
+ [Cached(Type = typeof(IRoomManager))]
+ protected TestRoomManager RoomManager { get; } = new TestRoomManager();
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("clear rooms", () => RoomManager.Rooms.Clear());
+ }
+
+ protected void AddRooms(int count, RulesetInfo ruleset = null)
+ {
+ AddStep("add rooms", () =>
+ {
+ for (int i = 0; i < count; i++)
+ {
+ var room = new Room
+ {
+ RoomID = { Value = i },
+ Name = { Value = $"Room {i}" },
+ Host = { Value = new User { Username = "Host" } },
+ EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) },
+ Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }
+ };
+
+ if (ruleset != null)
+ {
+ room.Playlist.Add(new PlaylistItem
+ {
+ Ruleset = { Value = ruleset },
+ Beatmap =
+ {
+ Value = new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata()
+ }
+ }
+ });
+ }
+
+ RoomManager.Rooms.Add(room);
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs
new file mode 100644
index 0000000000..67a53307fc
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs
@@ -0,0 +1,35 @@
+// 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.Online.Multiplayer;
+using osu.Game.Screens.Multi;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestRoomManager : IRoomManager
+ {
+ public event Action RoomsUpdated
+ {
+ add { }
+ remove { }
+ }
+
+ public readonly BindableList Rooms = new BindableList();
+
+ public Bindable InitialRoomsReceived { get; } = new Bindable(true);
+
+ IBindableList IRoomManager.Rooms => Rooms;
+
+ public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room);
+
+ public void JoinRoom(Room room, Action onSuccess = null, Action onError = null)
+ {
+ }
+
+ public void PartRoom()
+ {
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs
new file mode 100644
index 0000000000..7c0c2797f5
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Screens.Multi.Lounge.Components;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneLoungeFilterControl : OsuTestScene
+ {
+ public TestSceneLoungeFilterControl()
+ {
+ Child = new FilterControl
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
index 8b74eb5f27..cdad37a9ad 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public void Setup() => Schedule(() =>
{
- Room.CopyFrom(new Room());
+ Room = new Room();
Child = new RoomInfo
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 83f2297bd2..b1f6ee3e3a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -1,30 +1,22 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
-using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
-using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Lounge.Components;
-using osu.Game.Users;
using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
- public class TestSceneLoungeRoomsContainer : MultiplayerTestScene
+ public class TestSceneLoungeRoomsContainer : RoomManagerTestScene
{
- [Cached(Type = typeof(IRoomManager))]
- private TestRoomManager roomManager = new TestRoomManager();
-
private RoomsContainer container;
[BackgroundDependencyLoader]
@@ -39,34 +31,57 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
}
- public override void SetUpSteps()
- {
- base.SetUpSteps();
-
- AddStep("clear rooms", () => roomManager.Rooms.Clear());
- }
-
[Test]
public void TestBasicListChanges()
{
- addRooms(3);
+ AddRooms(3);
AddAssert("has 3 rooms", () => container.Rooms.Count == 3);
- AddStep("remove first room", () => roomManager.Rooms.Remove(roomManager.Rooms.FirstOrDefault()));
+ AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault()));
AddAssert("has 2 rooms", () => container.Rooms.Count == 2);
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID.Value != 0));
AddStep("select first room", () => container.Rooms.First().Action?.Invoke());
- AddAssert("first room selected", () => Room == roomManager.Rooms.First());
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
AddStep("join first room", () => container.Rooms.First().Action?.Invoke());
- AddAssert("first room joined", () => roomManager.Rooms.First().Status.Value is JoinedRoomStatus);
+ AddAssert("first room joined", () => RoomManager.Rooms.First().Status.Value is JoinedRoomStatus);
+ }
+
+ [Test]
+ public void TestKeyboardNavigation()
+ {
+ AddRooms(3);
+
+ AddAssert("no selection", () => checkRoomSelected(null));
+
+ press(Key.Down);
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+
+ press(Key.Up);
+ AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
+
+ press(Key.Down);
+ press(Key.Down);
+ AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last()));
+
+ press(Key.Enter);
+ AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus);
+ }
+
+ private void press(Key down)
+ {
+ AddStep($"press {down}", () =>
+ {
+ InputManager.PressKey(down);
+ InputManager.ReleaseKey(down);
+ });
}
[Test]
public void TestStringFiltering()
{
- addRooms(4);
+ AddRooms(4);
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
@@ -82,8 +97,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRulesetFiltering()
{
- addRooms(2, new OsuRuleset().RulesetInfo);
- addRooms(3, new CatchRuleset().RulesetInfo);
+ AddRooms(2, new OsuRuleset().RulesetInfo);
+ AddRooms(3, new CatchRuleset().RulesetInfo);
AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
@@ -96,67 +111,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
- private void addRooms(int count, RulesetInfo ruleset = null)
- {
- AddStep("add rooms", () =>
- {
- for (int i = 0; i < count; i++)
- {
- var room = new Room
- {
- RoomID = { Value = i },
- Name = { Value = $"Room {i}" },
- Host = { Value = new User { Username = "Host" } },
- EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) }
- };
-
- if (ruleset != null)
- {
- room.Playlist.Add(new PlaylistItem
- {
- Ruleset = { Value = ruleset },
- Beatmap =
- {
- Value = new BeatmapInfo
- {
- Metadata = new BeatmapMetadata()
- }
- }
- });
- }
-
- roomManager.Rooms.Add(room);
- }
- });
- }
+ private bool checkRoomSelected(Room room) => Room == room;
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();
- private class TestRoomManager : IRoomManager
- {
- public event Action RoomsUpdated
- {
- add { }
- remove { }
- }
-
- public readonly BindableList Rooms = new BindableList();
-
- public Bindable InitialRoomsReceived { get; } = new Bindable(true);
-
- IBindableList IRoomManager.Rooms => Rooms;
-
- public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room);
-
- public void JoinRoom(Room room, Action onSuccess = null, Action onError = null)
- {
- }
-
- public void PartRoom()
- {
- }
- }
-
private class JoinedRoomStatus : RoomStatus
{
public override string Message => "Joined";
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs
new file mode 100644
index 0000000000..68987127d2
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs
@@ -0,0 +1,58 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Graphics.Containers;
+using osu.Game.Screens.Multi.Lounge;
+using osu.Game.Screens.Multi.Lounge.Components;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneLoungeSubScreen : RoomManagerTestScene
+ {
+ private LoungeSubScreen loungeScreen;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("push screen", () => LoadScreen(loungeScreen = new LoungeSubScreen
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 0.5f,
+ }));
+
+ AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
+ }
+
+ private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType().First();
+
+ [Test]
+ public void TestScrollSelectedIntoView()
+ {
+ AddRooms(30);
+
+ AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First()));
+
+ AddStep("select last room", () => roomsContainer.Rooms.Last().Action?.Invoke());
+
+ AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms.First()));
+ AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms.Last()));
+ }
+
+ private bool checkRoomVisible(DrawableRoom room) =>
+ loungeScreen.ChildrenOfType().First().ScreenSpaceDrawQuad
+ .Contains(room.ScreenSpaceDrawQuad.Centre);
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
index 24d9f5ab12..01cd26fbe5 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public void Setup() => Schedule(() =>
{
- Room.Playlist.Clear();
+ Room = new Room();
Child = new MatchBeatmapDetailArea
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
index 38eb3181bf..e5943105b7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs
@@ -14,6 +14,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public TestSceneMatchHeader()
{
+ Room = new Room();
Room.Playlist.Add(new PlaylistItem
{
Beatmap =
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
index 7ba1782a28..c24c6c4ba3 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs
@@ -6,6 +6,7 @@ using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API;
+using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Users;
using osuTK;
@@ -18,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public TestSceneMatchLeaderboard()
{
- Room.RoomID.Value = 3;
+ Room = new Room { RoomID = { Value = 3 } };
Add(new MatchLeaderboard
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs
deleted file mode 100644
index 72bbc11cd0..0000000000
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboardChatDisplay.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Screens.Multi.Match.Components;
-using osuTK;
-
-namespace osu.Game.Tests.Visual.Multiplayer
-{
- public class TestSceneMatchLeaderboardChatDisplay : MultiplayerTestScene
- {
- protected override bool UseOnlineAPI => true;
-
- public TestSceneMatchLeaderboardChatDisplay()
- {
- Room.RoomID.Value = 7;
-
- Add(new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500),
- Child = new LeaderboardChatDisplay
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }
- });
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
index 5cff2d7d05..c62479faa0 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs
@@ -14,6 +14,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Multi.Components;
@@ -95,7 +96,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public void Setup() => Schedule(() =>
{
- Room.Playlist.Clear();
+ Room = new Room();
});
[Test]
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
index b687724105..2e22317539 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public void Setup() => Schedule(() =>
{
- Room.CopyFrom(new Room());
+ Room = new Room();
});
[SetUpSteps]
@@ -58,6 +58,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
+ [Test]
+ public void TestLoadSimpleMatch()
+ {
+ AddStep("set room properties", () =>
+ {
+ Room.RoomID.Value = 1;
+ Room.Name.Value = "my awesome room";
+ Room.Host.Value = new User { Id = 2, Username = "peppy" };
+ Room.RecentParticipants.Add(Room.Host.Value);
+ Room.Playlist.Add(new PlaylistItem
+ {
+ Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo }
+ });
+ });
+ }
+
[Test]
public void TestPlaylistItemSelectedOnCreate()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs
index 7ea3bba23f..b6bfa7c93a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
using osuTK;
@@ -12,22 +13,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
protected override bool UseOnlineAPI => true;
- public TestSceneOverlinedParticipants()
+ [SetUp]
+ public void Setup() => Schedule(() =>
{
- Room.RoomID.Value = 7;
- }
+ Room = new Room { RoomID = { Value = 7 } };
+ });
[Test]
public void TestHorizontalLayout()
{
AddStep("create component", () =>
{
- Child = new OverlinedParticipants(Direction.Horizontal)
+ Child = new ParticipantsDisplay(Direction.Horizontal)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
- AutoSizeAxes = Axes.Y,
};
});
}
@@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create component", () =>
{
- Child = new OverlinedParticipants(Direction.Vertical)
+ Child = new ParticipantsDisplay(Direction.Vertical)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs
index 14b7934dc7..14984988cb 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs
@@ -4,7 +4,7 @@
using osu.Framework.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
-using osu.Game.Screens.Multi.Components;
+using osu.Game.Screens.Multi;
using osu.Game.Tests.Beatmaps;
using osuTK;
@@ -16,6 +16,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public TestSceneOverlinedPlaylist()
{
+ Room = new Room { RoomID = { Value = 7 } };
+
for (int i = 0; i < 10; i++)
{
Room.Playlist.Add(new PlaylistItem
@@ -26,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
- Add(new OverlinedPlaylist(false)
+ Add(new DrawableRoomPlaylist(false, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs
index 9c4c45f94a..f71c5fc5d2 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using NUnit.Framework;
using osu.Framework.Graphics;
+using osu.Game.Online.Multiplayer;
using osu.Game.Screens.Multi.Components;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -10,10 +12,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
protected override bool UseOnlineAPI => true;
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Room = new Room { RoomID = { Value = 7 } };
+ });
+
public TestSceneParticipantsList()
{
- Room.RoomID.Value = 7;
-
Add(new ParticipantsList { RelativeSizeAxes = Axes.Both });
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
index 9fc7c336cb..03fd2b968c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
@@ -3,8 +3,16 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Net;
using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Newtonsoft.Json.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
@@ -12,6 +20,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Ranking;
+using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
@@ -19,43 +28,134 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneTimeshiftResultsScreen : ScreenTestScene
{
- private bool roomsReceived;
+ private const int scores_per_result = 10;
+
+ private TestResultsScreen resultsScreen;
+ private int currentScoreId;
+ private bool requestComplete;
[SetUp]
public void Setup() => Schedule(() =>
{
- roomsReceived = false;
+ currentScoreId = 0;
+ requestComplete = false;
bindHandler();
});
[Test]
- public void TestShowResultsWithScore()
+ public void TestShowWithUserScore()
{
- createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo));
- AddWaitStep("wait for display", 5);
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(userScore: userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
[Test]
- public void TestShowResultsNullScore()
+ public void TestShowNullUserScore()
{
- createResults(null);
- AddWaitStep("wait for display", 5);
+ createResults();
+ waitForDisplay();
+
+ AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
[Test]
- public void TestShowResultsNullScoreWithDelay()
+ public void TestShowUserScoreWithDelay()
+ {
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(3000, userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1);
+ AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
+ }
+
+ [Test]
+ public void TestShowNullUserScoreWithDelay()
{
AddStep("bind delayed handler", () => bindHandler(3000));
- createResults(null);
- AddUntilStep("wait for rooms to be received", () => roomsReceived);
- AddWaitStep("wait for display", 5);
+
+ createResults();
+ waitForDisplay();
+
+ AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
- private void createResults(ScoreInfo score)
+ [Test]
+ public void TestFetchWhenScrolledToTheRight()
+ {
+ createResults();
+ waitForDisplay();
+
+ AddStep("bind delayed handler", () => bindHandler(3000));
+
+ for (int i = 0; i < 2; i++)
+ {
+ int beforePanelCount = 0;
+
+ AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count());
+ AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false));
+
+ AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
+ waitForDisplay();
+
+ AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result);
+ AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
+ }
+ }
+
+ [Test]
+ public void TestFetchWhenScrolledToTheLeft()
+ {
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(userScore: userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddStep("bind delayed handler", () => bindHandler(3000));
+
+ for (int i = 0; i < 2; i++)
+ {
+ int beforePanelCount = 0;
+
+ AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count());
+ AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToStart(false));
+
+ AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
+ waitForDisplay();
+
+ AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result);
+ AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
+ }
+ }
+
+ private void createResults(Func getScore = null)
{
AddStep("load results", () =>
{
- LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem
+ LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@@ -63,62 +163,214 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
- private void bindHandler(double delay = 0)
+ private void waitForDisplay()
{
- var roomScores = new List();
+ AddUntilStep("wait for request to complete", () => requestComplete);
+ AddWaitStep("wait for display", 5);
+ }
- for (int i = 0; i < 10; i++)
+ private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
+ {
+ requestComplete = false;
+
+ if (failRequests)
{
- roomScores.Add(new RoomScore
+ triggerFail(request, delay);
+ return;
+ }
+
+ switch (request)
+ {
+ case ShowPlaylistUserScoreRequest s:
+ if (userScore == null)
+ triggerFail(s, delay);
+ else
+ triggerSuccess(s, createUserResponse(userScore), delay);
+ break;
+
+ case IndexPlaylistScoresRequest i:
+ triggerSuccess(i, createIndexResponse(i), delay);
+ break;
+ }
+ };
+
+ private void triggerSuccess(APIRequest req, T result, double delay)
+ where T : class
+ {
+ if (delay == 0)
+ success();
+ else
+ {
+ Task.Run(async () =>
{
- ID = i,
- Accuracy = 0.9 - 0.01 * i,
- EndedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromHours(i)),
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+ Schedule(success);
+ });
+ }
+
+ void success()
+ {
+ requestComplete = true;
+ req.TriggerSuccess(result);
+ }
+ }
+
+ private void triggerFail(APIRequest req, double delay)
+ {
+ if (delay == 0)
+ fail();
+ else
+ {
+ Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+ Schedule(fail);
+ });
+ }
+
+ void fail()
+ {
+ requestComplete = true;
+ req.TriggerFailure(new WebException("Failed."));
+ }
+ }
+
+ private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)
+ {
+ var multiplayerUserScore = new MultiplayerScore
+ {
+ ID = (int)(userScore.OnlineScoreID ?? currentScoreId++),
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
+ Passed = userScore.Passed,
+ Rank = userScore.Rank,
+ Position = 200,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore,
+ User = userScore.User,
+ Statistics = userScore.Statistics,
+ ScoresAround = new MultiplayerScoresAround
+ {
+ Higher = new MultiplayerScores(),
+ Lower = new MultiplayerScores()
+ }
+ };
+
+ for (int i = 1; i <= scores_per_result; i++)
+ {
+ multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
Passed = true,
- Rank = ScoreRank.B,
- MaxCombo = 999,
- TotalScore = 999999 - i * 1000,
+ Rank = userScore.Rank,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore - i,
User = new User
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
- Statistics =
+ Statistics = userScore.Statistics
+ });
+
+ multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
+ Passed = true,
+ Rank = userScore.Rank,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore + i,
+ User = new User
+ {
+ Id = 2,
+ Username = $"peppy{i}",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ },
+ Statistics = userScore.Statistics
+ });
+ }
+
+ addCursor(multiplayerUserScore.ScoresAround.Lower);
+ addCursor(multiplayerUserScore.ScoresAround.Higher);
+
+ return multiplayerUserScore;
+ }
+
+ private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req)
+ {
+ var result = new IndexedMultiplayerScores();
+
+ long startTotalScore = req.Cursor?.Properties["total_score"].ToObject() ?? 1000000;
+ string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc";
+
+ for (int i = 1; i <= scores_per_result; i++)
+ {
+ result.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = 1,
+ EndedAt = DateTimeOffset.Now,
+ Passed = true,
+ Rank = ScoreRank.X,
+ MaxCombo = 1000,
+ TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
+ User = new User
+ {
+ Id = 2,
+ Username = $"peppy{i}",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ },
+ Statistics = new Dictionary
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
- { HitResult.Great, 300 },
+ { HitResult.Great, 300 }
}
});
}
- ((DummyAPIAccess)API).HandleRequest = request =>
+ addCursor(result);
+
+ return result;
+ }
+
+ private void addCursor(MultiplayerScores scores)
+ {
+ scores.Cursor = new Cursor
{
- switch (request)
+ Properties = new Dictionary
{
- case GetRoomPlaylistScoresRequest r:
- if (delay == 0)
- success();
- else
- {
- Task.Run(async () =>
- {
- await Task.Delay(TimeSpan.FromMilliseconds(delay));
- Schedule(success);
- });
- }
+ { "total_score", JToken.FromObject(scores.Scores[^1].TotalScore) },
+ { "score_id", JToken.FromObject(scores.Scores[^1].ID) },
+ }
+ };
- void success()
- {
- r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores });
- roomsReceived = true;
- }
-
- break;
+ scores.Params = new IndexScoresParams
+ {
+ Properties = new Dictionary
+ {
+ { "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
}
};
}
+
+ private class TestResultsScreen : TimeshiftResultsScreen
+ {
+ public new LoadingSpinner LeftSpinner => base.LeftSpinner;
+ public new LoadingSpinner CentreSpinner => base.CentreSpinner;
+ public new LoadingSpinner RightSpinner => base.RightSpinner;
+ public new ScorePanelList ScorePanelList => base.ScorePanelList;
+
+ public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
+ : base(score, roomId, playlistItem, allowRetry)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 9d603ac471..8ccaca8630 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@@ -70,6 +71,23 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime);
}
+ [Test]
+ public void TestMenuMakesMusic()
+ {
+ WorkingBeatmap beatmap() => Game.Beatmap.Value;
+ Track track() => beatmap().Track;
+
+ TestSongSelect songSelect = null;
+
+ PushAndConfirm(() => songSelect = new TestSongSelect());
+
+ AddUntilStep("wait for no track", () => track() is TrackVirtual);
+
+ AddStep("return to menu", () => songSelect.Exit());
+
+ AddUntilStep("wait for track", () => !(track() is TrackVirtual) && track().IsRunning);
+ }
+
[Test]
public void TestExitSongSelectWithClick()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs
new file mode 100644
index 0000000000..17675bfbc0
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Overlays.News;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Framework.Allocation;
+using osu.Game.Overlays;
+using osuTK;
+using System;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneNewsCard : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ public TestSceneNewsCard()
+ {
+ Add(new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Direction = FillDirection.Vertical,
+ Width = 500,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(0, 20),
+ Children = new[]
+ {
+ new NewsCard(new APINewsPost
+ {
+ Title = "This post has an image which starts with \"/\" and has many authors! (clickable)",
+ Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ Author = "someone, someone1, someone2, someone3, someone4",
+ FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
+ PublishedAt = DateTimeOffset.Now,
+ Slug = "2020-07-16-summer-theme-park-2020-voting-open"
+ }),
+ new NewsCard(new APINewsPost
+ {
+ Title = "This post has a full-url image! (HTML entity: &) (non-clickable)",
+ Preview = "boom (HTML entity: &)",
+ Author = "user (HTML entity: &)",
+ FirstImage = "https://assets.ppy.sh/artists/88/header.jpg",
+ PublishedAt = DateTimeOffset.Now
+ })
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs
new file mode 100644
index 0000000000..78288bf6e4
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Overlays.News;
+using osu.Framework.Graphics;
+using osu.Game.Overlays;
+using osu.Framework.Allocation;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneNewsHeader : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ private TestHeader header;
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ Child = header = new TestHeader
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ };
+ });
+
+ [Test]
+ public void TestControl()
+ {
+ AddAssert("Front page selected", () => header.Current.Value == "frontpage");
+ AddAssert("1 tab total", () => header.TabCount == 1);
+
+ AddStep("Set article 1", () => header.SetArticle("1"));
+ AddAssert("Article 1 selected", () => header.Current.Value == "1");
+ AddAssert("2 tabs total", () => header.TabCount == 2);
+
+ AddStep("Set article 2", () => header.SetArticle("2"));
+ AddAssert("Article 2 selected", () => header.Current.Value == "2");
+ AddAssert("2 tabs total", () => header.TabCount == 2);
+
+ AddStep("Set front page", () => header.SetFrontPage());
+ AddAssert("Front page selected", () => header.Current.Value == "frontpage");
+ AddAssert("1 tab total", () => header.TabCount == 1);
+ }
+
+ private class TestHeader : NewsHeader
+ {
+ public int TabCount => TabControl.Items.Count;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
index d47c972564..37d51c16d2 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
@@ -2,65 +2,64 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Graphics;
+using NUnit.Framework;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
-using osu.Game.Overlays.News;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneNewsOverlay : OsuTestScene
{
- private TestNewsOverlay news;
+ private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
- protected override void LoadComplete()
+ private NewsOverlay news;
+
+ [SetUp]
+ public void SetUp() => Schedule(() => Child = news = new NewsOverlay());
+
+ [Test]
+ public void TestRequest()
{
- base.LoadComplete();
- Add(news = new TestNewsOverlay());
- AddStep(@"Show", news.Show);
- AddStep(@"Hide", news.Hide);
-
- AddStep(@"Show front page", () => news.ShowFrontPage());
- AddStep(@"Custom article", () => news.Current.Value = "Test Article 101");
-
- AddStep(@"Article covers", () => news.LoadAndShowContent(new NewsCoverTest()));
+ setUpNewsResponse(responseExample);
+ AddStep("Show", () => news.Show());
+ AddStep("Show article", () => news.ShowArticle("article"));
}
- private class TestNewsOverlay : NewsOverlay
- {
- public new void LoadAndShowContent(NewsContent content) => base.LoadAndShowContent(content);
- }
-
- private class NewsCoverTest : NewsContent
- {
- public NewsCoverTest()
+ private void setUpNewsResponse(GetNewsResponse r)
+ => AddStep("set up response", () =>
{
- Spacing = new osuTK.Vector2(0, 10);
-
- var article = new NewsArticleCover.ArticleInfo
+ dummyAPI.HandleRequest = request =>
{
- Author = "Ephemeral",
- CoverUrl = "https://assets.ppy.sh/artists/58/header.jpg",
- Time = new DateTime(2019, 12, 4),
- Title = "New Featured Artist: Kurokotei"
- };
+ if (!(request is GetNewsRequest getNewsRequest))
+ return;
- Children = new Drawable[]
- {
- new NewsArticleCover(article)
- {
- Height = 200
- },
- new NewsArticleCover(article)
- {
- Height = 120
- },
- new NewsArticleCover(article)
- {
- RelativeSizeAxes = Axes.None,
- Size = new osuTK.Vector2(400, 200),
- }
+ getNewsRequest.TriggerSuccess(r);
};
+ });
+
+ private GetNewsResponse responseExample => new GetNewsResponse
+ {
+ NewsPosts = new[]
+ {
+ new APINewsPost
+ {
+ Title = "This post has an image which starts with \"/\" and has many authors!",
+ Preview = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ Author = "someone, someone1, someone2, someone3, someone4",
+ FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png",
+ PublishedAt = DateTimeOffset.Now
+ },
+ new APINewsPost
+ {
+ Title = "This post has a full-url image! (HTML entity: &)",
+ Preview = "boom (HTML entity: &)",
+ Author = "user (HTML entity: &)",
+ FirstImage = "https://assets.ppy.sh/artists/88/header.jpg",
+ PublishedAt = DateTimeOffset.Now
+ }
}
- }
+ };
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs
index 997db827f3..d60222fa0b 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs
@@ -30,12 +30,6 @@ namespace osu.Game.Tests.Visual.Online
Add(selector = new SpotlightSelector());
}
- [Test]
- public void TestVisibility()
- {
- AddStep("Toggle Visibility", selector.ToggleVisibility);
- }
-
[Test]
public void TestLocalSpotlights()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
index a3b102dc76..ee109189c7 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch;
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
+using osu.Game.Overlays.Rankings;
namespace osu.Game.Tests.Visual.Online
{
@@ -105,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
{
onLoadStarted();
- request = new GetSpotlightRankingsRequest(ruleset, spotlight);
+ request = new GetSpotlightRankingsRequest(ruleset, spotlight, RankingsSortCriteria.All);
((GetSpotlightRankingsRequest)request).Success += rankings => Schedule(() =>
{
var table = new ScoresTable(1, rankings.Users);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
index 273f593c32..18ac415126 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
@@ -4,19 +4,22 @@
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation;
-using osu.Game.Graphics;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneShowMoreButton : OsuTestScene
{
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
public TestSceneShowMoreButton()
{
- TestButton button = null;
+ ShowMoreButton button = null;
int fireCount = 0;
- Add(button = new TestButton
+ Add(button = new ShowMoreButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -46,16 +49,5 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("action fired twice", () => fireCount == 2);
AddAssert("is in loading state", () => button.IsLoading);
}
-
- private class TestButton : ShowMoreButton
- {
- [BackgroundDependencyLoader]
- private void load(OsuColour colors)
- {
- IdleColour = colors.YellowDark;
- HoverColour = colors.Yellow;
- ChevronIconColour = colors.Red;
- }
- }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs
deleted file mode 100644
index 77e77d90c1..0000000000
--- a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using NUnit.Framework;
-using osu.Game.Overlays;
-using osu.Game.Users;
-
-namespace osu.Game.Tests.Visual.Online
-{
- [TestFixture]
- public class TestSceneSocialOverlay : OsuTestScene
- {
- protected override bool UseOnlineAPI => true;
-
- public TestSceneSocialOverlay()
- {
- SocialOverlay s = new SocialOverlay
- {
- Users = new[]
- {
- new User
- {
- Username = @"flyte",
- Id = 3103765,
- Country = new Country { FlagName = @"JP" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
- },
- new User
- {
- Username = @"Cookiezi",
- Id = 124493,
- Country = new Country { FlagName = @"KR" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
- },
- new User
- {
- Username = @"Angelsim",
- Id = 1777162,
- Country = new Country { FlagName = @"KR" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
- },
- new User
- {
- Username = @"Rafis",
- Id = 2558286,
- Country = new Country { FlagName = @"PL" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg",
- },
- new User
- {
- Username = @"hvick225",
- Id = 50265,
- Country = new Country { FlagName = @"TW" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg",
- },
- new User
- {
- Username = @"peppy",
- Id = 2,
- Country = new Country { FlagName = @"AU" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
- },
- new User
- {
- Username = @"filsdelama",
- Id = 2831793,
- Country = new Country { FlagName = @"FR" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c7.jpg"
- },
- new User
- {
- Username = @"_index",
- Id = 652457,
- Country = new Country { FlagName = @"RU" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c8.jpg"
- },
- },
- };
- Add(s);
-
- AddStep(@"toggle", s.ToggleVisibility);
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
index f763e50067..c2e9945c99 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
@@ -42,6 +42,19 @@ namespace osu.Game.Tests.Visual.Online
Spacing = new Vector2(10f),
Children = new Drawable[]
{
+ new UserBrickPanel(new User
+ {
+ Username = @"flyte",
+ Id = 3103765,
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
+ }),
+ new UserBrickPanel(new User
+ {
+ Username = @"peppy",
+ Id = 2,
+ Colour = "99EB47",
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }),
flyte = new UserGridPanel(new User
{
Username = @"flyte",
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
new file mode 100644
index 0000000000..7ca1fc842f
--- /dev/null
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
@@ -0,0 +1,71 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Ranking.Statistics;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Ranking
+{
+ public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
+ {
+ [Test]
+ public void TestManyDistributedEvents()
+ {
+ createTest(CreateDistributedHitEvents());
+ }
+
+ [Test]
+ public void TestZeroTimeOffset()
+ {
+ createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList());
+ }
+
+ [Test]
+ public void TestNoEvents()
+ {
+ createTest(new List());
+ }
+
+ private void createTest(List events) => AddStep("create test", () =>
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex("#333")
+ },
+ new HitEventTimingDistributionGraph(events)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(600, 130)
+ }
+ };
+ });
+
+ public static List CreateDistributedHitEvents()
+ {
+ var hitEvents = new List();
+
+ for (int i = 0; i < 50; i++)
+ {
+ int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2));
+
+ for (int j = 0; j < count; j++)
+ hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null));
+ }
+
+ return hitEvents;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index 125aa0a1e7..74808bc2f5 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -1,23 +1,32 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
+using osu.Game.Screens.Ranking.Statistics;
+using osuTK;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking
{
[TestFixture]
- public class TestSceneResultsScreen : ScreenTestScene
+ public class TestSceneResultsScreen : OsuManualInputManagerTestScene
{
private BeatmapManager beatmaps;
@@ -41,7 +50,7 @@ namespace osu.Game.Tests.Visual.Ranking
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
[Test]
- public void ResultsWithoutPlayer()
+ public void TestResultsWithoutPlayer()
{
TestResultsScreen screen = null;
OsuScreenStack stack;
@@ -60,7 +69,7 @@ namespace osu.Game.Tests.Visual.Ranking
}
[Test]
- public void ResultsWithPlayer()
+ public void TestResultsWithPlayer()
{
TestResultsScreen screen = null;
@@ -70,7 +79,7 @@ namespace osu.Game.Tests.Visual.Ranking
}
[Test]
- public void ResultsForUnranked()
+ public void TestResultsForUnranked()
{
UnrankedSoloResultsScreen screen = null;
@@ -79,6 +88,130 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
+ [Test]
+ public void TestShowHideStatisticsViaOutsideClick()
+ {
+ TestResultsScreen screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+
+ AddStep("click expanded panel", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ InputManager.MoveMouseTo(expandedPanel);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible);
+
+ AddUntilStep("expanded panel at the left of the screen", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150;
+ });
+
+ AddStep("click to right of panel", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("statistics hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
+
+ AddUntilStep("expanded panel in centre of screen", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1);
+ });
+ }
+
+ [Test]
+ public void TestShowHideStatistics()
+ {
+ TestResultsScreen screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+
+ AddStep("click expanded panel", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ InputManager.MoveMouseTo(expandedPanel);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible);
+
+ AddUntilStep("expanded panel at the left of the screen", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150;
+ });
+
+ AddStep("click expanded panel", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ InputManager.MoveMouseTo(expandedPanel);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("statistics hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden);
+
+ AddUntilStep("expanded panel in centre of screen", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1);
+ });
+ }
+
+ [Test]
+ public void TestShowStatisticsAndClickOtherPanel()
+ {
+ TestResultsScreen screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+
+ ScorePanel expandedPanel = null;
+ ScorePanel contractedPanel = null;
+
+ AddStep("click expanded panel then contracted panel", () =>
+ {
+ expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ InputManager.MoveMouseTo(expandedPanel);
+ InputManager.Click(MouseButton.Left);
+
+ contractedPanel = this.ChildrenOfType().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X);
+ InputManager.MoveMouseTo(contractedPanel);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("statistics shown", () => this.ChildrenOfType().Single().State.Value == Visibility.Visible);
+
+ AddAssert("contracted panel still contracted", () => contractedPanel.State == PanelState.Contracted);
+ AddAssert("expanded panel still expanded", () => expandedPanel.State == PanelState.Expanded);
+ }
+
+ [Test]
+ public void TestFetchScoresAfterShowingStatistics()
+ {
+ DelayedFetchResultsScreen screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo), 3000)));
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddStep("click expanded panel", () =>
+ {
+ var expandedPanel = this.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ InputManager.MoveMouseTo(expandedPanel);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("wait for fetch", () => screen.FetchCompleted);
+ AddAssert("expanded panel still on screen", () => this.ChildrenOfType().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0);
+ }
+
private class TestResultsContainer : Container
{
[Cached(typeof(Player))]
@@ -113,6 +246,58 @@ namespace osu.Game.Tests.Visual.Ranking
RetryOverlay = InternalChildren.OfType().SingleOrDefault();
}
+
+ protected override APIRequest FetchScores(Action> scoresCallback)
+ {
+ var scores = new List();
+
+ for (int i = 0; i < 20; i++)
+ {
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ score.TotalScore += 10 - i;
+ scores.Add(score);
+ }
+
+ scoresCallback?.Invoke(scores);
+
+ return null;
+ }
+ }
+
+ private class DelayedFetchResultsScreen : TestResultsScreen
+ {
+ public bool FetchCompleted { get; private set; }
+
+ private readonly double delay;
+
+ public DelayedFetchResultsScreen(ScoreInfo score, double delay)
+ : base(score)
+ {
+ this.delay = delay;
+ }
+
+ protected override APIRequest FetchScores(Action> scoresCallback)
+ {
+ Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+
+ var scores = new List();
+
+ for (int i = 0; i < 20; i++)
+ {
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ score.TotalScore += 10 - i;
+ scores.Add(score);
+ }
+
+ scoresCallback?.Invoke(scores);
+
+ Schedule(() => FetchCompleted = true);
+ });
+
+ return null;
+ }
}
private class UnrankedSoloResultsScreen : SoloResultsScreen
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
new file mode 100644
index 0000000000..8700fbeb42
--- /dev/null
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
@@ -0,0 +1,48 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Screens.Ranking.Statistics;
+
+namespace osu.Game.Tests.Visual.Ranking
+{
+ public class TestSceneStatisticsPanel : OsuTestScene
+ {
+ [Test]
+ public void TestScoreWithStatistics()
+ {
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
+ {
+ HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents()
+ };
+
+ loadPanel(score);
+ }
+
+ [Test]
+ public void TestScoreWithoutStatistics()
+ {
+ loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo));
+ }
+
+ [Test]
+ public void TestNullScore()
+ {
+ loadPanel(null);
+ }
+
+ private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
+ {
+ Child = new StatisticsPanel
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Visibility.Visible },
+ Score = { Value = score }
+ };
+ });
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 073d75692e..a3ea4619cc 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -80,9 +80,9 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestRecommendedSelection()
{
- loadBeatmaps();
+ loadBeatmaps(carouselAdjust: carousel => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault());
- AddStep("set recommendation function", () => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault());
+ AddStep("select last", () => carousel.SelectBeatmap(carousel.BeatmapSets.Last().Beatmaps.Last()));
// check recommended was selected
advanceSelection(direction: 1, diff: false);
@@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
loadBeatmaps();
- advanceSelection(direction: 1, diff: false);
+ AddStep("select first", () => carousel.SelectBeatmap(carousel.BeatmapSets.First().Beatmaps.First()));
waitForSelection(1, 1);
advanceSelection(direction: 1, diff: true);
@@ -707,9 +707,9 @@ namespace osu.Game.Tests.Visual.SongSelect
checkVisibleItemCount(true, 15);
}
- private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null)
+ private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null)
{
- createCarousel();
+ createCarousel(carouselAdjust);
if (beatmapSets == null)
{
@@ -730,17 +730,21 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("Wait for load", () => changed);
}
- private void createCarousel(Container target = null)
+ private void createCarousel(Action carouselAdjust = null, Container target = null)
{
AddStep("Create carousel", () =>
{
selectedSets.Clear();
eagerSelectedIDs.Clear();
- (target ?? this).Child = carousel = new TestBeatmapCarousel
+ carousel = new TestBeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
};
+
+ carouselAdjust?.Invoke(carousel);
+
+ (target ?? this).Child = carousel;
});
}
diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
index 22ae5257e7..b347c39c1e 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
@@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual
typeof(NotificationOverlay),
typeof(BeatmapListingOverlay),
typeof(DashboardOverlay),
+ typeof(NewsOverlay),
typeof(ChannelManager),
typeof(ChatOverlay),
typeof(SettingsOverlay),
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs
new file mode 100644
index 0000000000..c2ac5179c9
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs
@@ -0,0 +1,67 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Overlays.Comments.Buttons;
+using osu.Framework.Graphics;
+using osu.Framework.Allocation;
+using osu.Game.Overlays;
+using osu.Framework.Graphics.Containers;
+using osuTK;
+using NUnit.Framework;
+using System.Linq;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneCommentRepliesButton : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
+
+ private readonly TestButton button;
+
+ public TestSceneCommentRepliesButton()
+ {
+ Child = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ Children = new Drawable[]
+ {
+ button = new TestButton(),
+ new LoadRepliesButton
+ {
+ Action = () => { }
+ },
+ new ShowRepliesButton(1),
+ new ShowRepliesButton(2)
+ }
+ };
+ }
+
+ [Test]
+ public void TestArrowDirection()
+ {
+ AddStep("Set upwards", () => button.SetIconDirection(true));
+ AddAssert("Icon facing upwards", () => button.Icon.Scale.Y == -1);
+ AddStep("Set downwards", () => button.SetIconDirection(false));
+ AddAssert("Icon facing downwards", () => button.Icon.Scale.Y == 1);
+ }
+
+ private class TestButton : CommentRepliesButton
+ {
+ public SpriteIcon Icon => this.ChildrenOfType().First();
+
+ public TestButton()
+ {
+ Text = "sample text";
+ }
+
+ public new void SetIconDirection(bool upwards) => base.SetIconDirection(upwards);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs
new file mode 100644
index 0000000000..c51204eaba
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs
@@ -0,0 +1,150 @@
+// 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.Containers;
+using osu.Framework.Graphics;
+using osu.Game.Overlays.Dashboard.Home;
+using osu.Game.Beatmaps;
+using osu.Game.Overlays;
+using osu.Framework.Allocation;
+using osu.Game.Users;
+using System;
+using osu.Framework.Graphics.Shapes;
+using System.Collections.Generic;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneDashboardBeatmapListing : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
+ private readonly Container content;
+
+ public TestSceneDashboardBeatmapListing()
+ {
+ Add(content = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Y,
+ Width = 300,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 10 },
+ Child = new DashboardBeatmapListing(new_beatmaps, popular_beatmaps)
+ }
+ }
+ });
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AddStep("Set width to 500", () => content.ResizeWidthTo(500, 500));
+ AddStep("Set width to 300", () => content.ResizeWidthTo(300, 500));
+ }
+
+ private static readonly List new_beatmaps = new List
+ {
+ new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Very Long Title (TV size) [TATOE]",
+ Artist = "This artist has a really long name how is this possible",
+ Author = new User
+ {
+ Username = "author",
+ Id = 100
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers
+ {
+ Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
+ },
+ Ranked = DateTimeOffset.Now
+ }
+ },
+ new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Very Long Title (TV size) [TATOE]",
+ Artist = "This artist has a really long name how is this possible",
+ Author = new User
+ {
+ Username = "author",
+ Id = 100
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers
+ {
+ Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608",
+ },
+ Ranked = DateTimeOffset.MinValue
+ }
+ }
+ };
+
+ private static readonly List popular_beatmaps = new List
+ {
+ new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Title",
+ Artist = "Artist",
+ Author = new User
+ {
+ Username = "author",
+ Id = 100
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers
+ {
+ Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586",
+ },
+ FavouriteCount = 100
+ }
+ },
+ new BeatmapSetInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Title 2",
+ Artist = "Artist 2",
+ Author = new User
+ {
+ Username = "someone",
+ Id = 100
+ }
+ },
+ OnlineInfo = new BeatmapSetOnlineInfo
+ {
+ Covers = new BeatmapSetOnlineCovers
+ {
+ Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586",
+ },
+ FavouriteCount = 10
+ }
+ }
+ };
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs
new file mode 100644
index 0000000000..155d043bf9
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneLogoAnimation : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(LargeTextureStore textures)
+ {
+ LogoAnimation anim2;
+
+ Add(anim2 = new LogoAnimation
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Texture = textures.Get("Intro/Triangles/logo-highlight"),
+ Colour = Colour4.White,
+ });
+
+ LogoAnimation anim;
+
+ Add(anim = new LogoAnimation
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Texture = textures.Get("Intro/Triangles/logo-background"),
+ Colour = OsuColour.Gray(0.6f),
+ });
+
+ AddSliderStep("Progress", 0f, 1f, 0f, newValue =>
+ {
+ anim2.AnimationProgress = newValue;
+ anim.AnimationProgress = newValue;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
index 010e4330d7..5582cc6826 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -263,7 +264,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void moveLogoFacade()
{
- if (logoFacade?.Transforms.Count == 0 && transferContainer?.Transforms.Count == 0)
+ if (!(logoFacade?.Transforms).Any() && !(transferContainer?.Transforms).Any())
{
Random random = new Random();
trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index ce691bff70..6f083f4ab6 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -13,6 +13,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -99,6 +101,12 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestManiaMods()
{
changeRuleset(3);
+
+ var mania = new ManiaRuleset();
+
+ testModsWithSameBaseType(
+ mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)),
+ mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden)));
}
[Test]
@@ -197,6 +205,18 @@ namespace osu.Game.Tests.Visual.UserInterface
checkLabelColor(() => Color4.White);
}
+ private void testModsWithSameBaseType(Mod modA, Mod modB)
+ {
+ selectNext(modA);
+ checkSelected(modA);
+ selectNext(modB);
+ checkSelected(modB);
+
+ // Backwards
+ selectPrevious(modA);
+ checkSelected(modA);
+ }
+
private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1));
private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1));
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs
deleted file mode 100644
index 97a3f62b2d..0000000000
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNumberBox.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics.UserInterface;
-
-namespace osu.Game.Tests.Visual.UserInterface
-{
- [TestFixture]
- public class TestSceneNumberBox : OsuTestScene
- {
- private OsuNumberBox numberBox;
-
- [BackgroundDependencyLoader]
- private void load()
- {
- Child = new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- Padding = new MarginPadding { Horizontal = 250 },
- Child = numberBox = new OsuNumberBox
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.X,
- PlaceholderText = "Insert numbers here"
- }
- };
-
- clearInput();
- AddStep("enter numbers", () => numberBox.Text = "987654321");
- expectedValue("987654321");
- clearInput();
- AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3");
- expectedValue("123");
- clearInput();
- }
-
- private void clearInput() => AddStep("clear input", () => numberBox.Text = null);
-
- private void expectedValue(string value) => AddAssert("expect number", () => numberBox.Text == value);
- }
-}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs
new file mode 100644
index 0000000000..756928d3ec
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneOsuTextBox : OsuTestScene
+ {
+ private readonly OsuNumberBox numberBox;
+
+ public TestSceneOsuTextBox()
+ {
+ Child = new Container
+ {
+ Masking = true,
+ CornerRadius = 10f,
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding(15f),
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.DarkSlateGray,
+ Alpha = 0.75f,
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(50f),
+ Spacing = new Vector2(0f, 50f),
+ Children = new[]
+ {
+ new OsuTextBox
+ {
+ Width = 500f,
+ PlaceholderText = "Normal textbox",
+ },
+ new OsuPasswordTextBox
+ {
+ Width = 500f,
+ PlaceholderText = "Password textbox",
+ },
+ numberBox = new OsuNumberBox
+ {
+ Width = 500f,
+ PlaceholderText = "Number textbox"
+ }
+ }
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestNumberBox()
+ {
+ clearTextbox(numberBox);
+ AddStep("enter numbers", () => numberBox.Text = "987654321");
+ expectedValue(numberBox, "987654321");
+
+ clearTextbox(numberBox);
+ AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3");
+ expectedValue(numberBox, "123");
+
+ clearTextbox(numberBox);
+ }
+
+ private void clearTextbox(OsuTextBox textBox) => AddStep("clear textbox", () => textBox.Text = null);
+ private void expectedValue(OsuTextBox textBox, string value) => AddAssert("expected textbox value", () => textBox.Text == value);
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
index 60af5b37ef..2a76b8e265 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
@@ -36,11 +36,11 @@ namespace osu.Game.Tests.Visual.UserInterface
}
});
- addHeader("Orange OverlayHeader (no background)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange);
- addHeader("Blue OverlayHeader", new TestNoControlHeader(), OverlayColourScheme.Blue);
+ addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange);
+ addHeader("Blue OverlayHeader (default 50 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue);
addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green);
- addHeader("Pink TabControlOverlayHeader (enum)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink);
- addHeader("Red BreadcrumbControlOverlayHeader (no background)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red);
+ addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink);
+ addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red);
}
private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme)
@@ -86,6 +86,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestNoBackgroundHeader : OverlayHeader
{
protected override OverlayTitle CreateTitle() => new TestTitle();
+
+ public TestNoBackgroundHeader()
+ {
+ ContentSidePadding = 100;
+ }
}
private class TestNoControlHeader : OverlayHeader
@@ -112,6 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestEnumTabControlHeader : TabControlOverlayHeader
{
+ public TestEnumTabControlHeader()
+ {
+ ContentSidePadding = 30;
+ }
+
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
protected override OverlayTitle CreateTitle() => new TestTitle();
@@ -130,6 +140,8 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestBreadcrumbControlHeader()
{
+ ContentSidePadding = 10;
+
TabControl.AddItem("tab1");
TabControl.AddItem("tab2");
TabControl.Current.Value = "tab2";
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs
new file mode 100644
index 0000000000..24bc0dbc97
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Rankings;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneRankingsSortTabControl : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
+ public TestSceneRankingsSortTabControl()
+ {
+ Child = new RankingsSortTabControl
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 5ee887cb64..4b0506d818 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index aa37326a49..f256b8e4e9 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -7,7 +7,7 @@
-
+
WinExe
diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs
index fc7fcef892..cafec0a88b 100644
--- a/osu.Game.Tournament/Components/SongBar.cs
+++ b/osu.Game.Tournament/Components/SongBar.cs
@@ -6,6 +6,7 @@ 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.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
@@ -66,6 +67,9 @@ namespace osu.Game.Tournament.Components
}
}
+ // Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away.
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -77,8 +81,6 @@ namespace osu.Game.Tournament.Components
flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
- // Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away.
- Height = 1,
AutoSizeAxes = Axes.Y,
LayoutDuration = 500,
LayoutEasing = Easing.OutQuint,
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
index 4aea7ff4c0..fa530ea2c4 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{
}
- private class SettingsRoundDropdown : LadderSettingsDropdown
+ private class SettingsRoundDropdown : SettingsDropdown
{
public SettingsRoundDropdown(BindableList rounds)
{
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs
deleted file mode 100644
index 347e4d91e0..0000000000
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Graphics.UserInterface;
-using osu.Game.Overlays.Settings;
-
-namespace osu.Game.Tournament.Screens.Ladder.Components
-{
- public class LadderSettingsDropdown : SettingsDropdown
- {
- protected override OsuDropdown CreateDropdown() => new DropdownControl();
-
- private new class DropdownControl : SettingsDropdown.DropdownControl
- {
- protected override DropdownMenu CreateMenu() => new Menu();
-
- private new class Menu : OsuDropdownMenu
- {
- public Menu()
- {
- MaxHeight = 200;
- }
- }
- }
- }
-}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
index a630e51e44..6604e3a313 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
@@ -6,11 +6,12 @@ using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Ladder.Components
{
- public class SettingsTeamDropdown : LadderSettingsDropdown
+ public class SettingsTeamDropdown : SettingsDropdown
{
public SettingsTeamDropdown(BindableList teams)
{
diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
index d48e396b89..eed3cac9f0 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
@@ -203,13 +203,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = TournamentGame.TEXT_COLOUR,
+ Colour = TournamentGame.ELEMENT_BACKGROUND_COLOUR,
},
new TournamentSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = seeding.ToString("#,0"),
+ Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR
},
}
},
diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index 99e0bf4e33..11fee030f8 100644
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo;
- beatmap.HitObjects = convertHitObjects(original.HitObjects, original);
+ beatmap.HitObjects = convertHitObjects(original.HitObjects, original).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks;
return beatmap;
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
new file mode 100644
index 0000000000..b80b4e45ed
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
@@ -0,0 +1,303 @@
+// 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.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Lists;
+using osu.Framework.Threading;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Beatmaps
+{
+ public class BeatmapDifficultyManager : CompositeDrawable
+ {
+ // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes.
+ private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager));
+
+ // A permanent cache to prevent re-computations.
+ private readonly ConcurrentDictionary difficultyCache = new ConcurrentDictionary();
+
+ // All bindables that should be updated along with the current ruleset + mods.
+ private readonly LockedWeakList trackedBindables = new LockedWeakList();
+
+ [Resolved]
+ private BeatmapManager beatmapManager { get; set; }
+
+ [Resolved]
+ private Bindable currentRuleset { get; set; }
+
+ [Resolved]
+ private Bindable> currentMods { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ currentRuleset.BindValueChanged(_ => updateTrackedBindables());
+ currentMods.BindValueChanged(_ => updateTrackedBindables(), true);
+ }
+
+ ///
+ /// Retrieves a bindable containing the star difficulty of a that follows the currently-selected ruleset and mods.
+ ///
+ /// The to get the difficulty of.
+ /// An optional which stops updating the star difficulty for the given .
+ /// A bindable that is updated to contain the star difficulty when it becomes available.
+ public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
+ {
+ var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
+ trackedBindables.Add(bindable);
+ return bindable;
+ }
+
+ ///
+ /// Retrieves a bindable containing the star difficulty of a with a given and combination.
+ ///
+ ///
+ /// The bindable will not update to follow the currently-selected ruleset and mods.
+ ///
+ /// The to get the difficulty of.
+ /// The to get the difficulty with. If null, the 's ruleset is used.
+ /// The s to get the difficulty with. If null, no mods will be assumed.
+ /// An optional which stops updating the star difficulty for the given .
+ /// A bindable that is updated to contain the star difficulty when it becomes available.
+ public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods,
+ CancellationToken cancellationToken = default)
+ => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
+
+ ///
+ /// Retrieves the difficulty of a .
+ ///
+ /// The to get the difficulty of.
+ /// The to get the difficulty with.
+ /// The s to get the difficulty with.
+ /// An optional which stops computing the star difficulty.
+ /// The .
+ public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
+ return existing;
+
+ return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken,
+ TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
+ }
+
+ ///
+ /// Retrieves the difficulty of a .
+ ///
+ /// The to get the difficulty of.
+ /// The to get the difficulty with.
+ /// The s to get the difficulty with.
+ /// The .
+ public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null)
+ {
+ if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
+ return existing;
+
+ return computeDifficulty(key, beatmapInfo, rulesetInfo);
+ }
+
+ private CancellationTokenSource trackedUpdateCancellationSource;
+ private readonly List linkedCancellationSources = new List();
+
+ ///
+ /// Updates all tracked using the current ruleset and mods.
+ ///
+ private void updateTrackedBindables()
+ {
+ cancelTrackedBindableUpdate();
+ trackedUpdateCancellationSource = new CancellationTokenSource();
+
+ foreach (var b in trackedBindables)
+ {
+ var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken);
+ linkedCancellationSources.Add(linkedSource);
+
+ updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
+ }
+ }
+
+ ///
+ /// Cancels the existing update of all tracked via .
+ ///
+ private void cancelTrackedBindableUpdate()
+ {
+ trackedUpdateCancellationSource?.Cancel();
+ trackedUpdateCancellationSource = null;
+
+ if (linkedCancellationSources != null)
+ {
+ foreach (var c in linkedCancellationSources)
+ c.Dispose();
+
+ linkedCancellationSources.Clear();
+ }
+ }
+
+ ///
+ /// Creates a new and triggers an initial value update.
+ ///
+ /// The that star difficulty should correspond to.
+ /// The initial to get the difficulty with.
+ /// The initial s to get the difficulty with.
+ /// An optional which stops updating the star difficulty for the given .
+ /// The .
+ private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods,
+ CancellationToken cancellationToken)
+ {
+ var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken);
+ updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken);
+ return bindable;
+ }
+
+ ///
+ /// Updates the value of a with a given ruleset + mods.
+ ///
+ /// The to update.
+ /// The to update with.
+ /// The s to update with.
+ /// A token that may be used to cancel this update.
+ private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default)
+ {
+ GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t =>
+ {
+ // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
+ Schedule(() =>
+ {
+ if (!cancellationToken.IsCancellationRequested)
+ bindable.Value = t.Result;
+ });
+ }, cancellationToken);
+ }
+
+ ///
+ /// Computes the difficulty defined by a key, and stores it to the timed cache.
+ ///
+ /// The that defines the computation parameters.
+ /// The to compute the difficulty of.
+ /// The to compute the difficulty with.
+ /// The .
+ private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo)
+ {
+ // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
+ rulesetInfo ??= beatmapInfo.Ruleset;
+
+ try
+ {
+ var ruleset = rulesetInfo.CreateInstance();
+ Debug.Assert(ruleset != null);
+
+ var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo));
+ var attributes = calculator.Calculate(key.Mods);
+
+ return difficultyCache[key] = new StarDifficulty(attributes.StarRating);
+ }
+ catch
+ {
+ return difficultyCache[key] = new StarDifficulty(0);
+ }
+ }
+
+ ///
+ /// Attempts to retrieve an existing difficulty for the combination.
+ ///
+ /// The .
+ /// The .
+ /// The s.
+ /// The existing difficulty value, if present.
+ /// The key that was used to perform this lookup. This can be further used to query .
+ /// Whether an existing difficulty was found.
+ private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key)
+ {
+ // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
+ rulesetInfo ??= beatmapInfo.Ruleset;
+
+ // Difficulty can only be computed if the beatmap and ruleset are locally available.
+ if (beatmapInfo.ID == 0 || rulesetInfo.ID == null)
+ {
+ // If not, fall back to the existing star difficulty (e.g. from an online source).
+ existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty);
+ key = default;
+
+ return true;
+ }
+
+ key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods);
+ return difficultyCache.TryGetValue(key, out existingDifficulty);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ cancelTrackedBindableUpdate();
+ updateScheduler?.Dispose();
+ }
+
+ private readonly struct DifficultyCacheLookup : IEquatable
+ {
+ public readonly int BeatmapId;
+ public readonly int RulesetId;
+ public readonly Mod[] Mods;
+
+ public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable mods)
+ {
+ BeatmapId = beatmapId;
+ RulesetId = rulesetId;
+ Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty();
+ }
+
+ public bool Equals(DifficultyCacheLookup other)
+ => BeatmapId == other.BeatmapId
+ && RulesetId == other.RulesetId
+ && Mods.SequenceEqual(other.Mods);
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+
+ hashCode.Add(BeatmapId);
+ hashCode.Add(RulesetId);
+ foreach (var mod in Mods)
+ hashCode.Add(mod.Acronym);
+
+ return hashCode.ToHashCode();
+ }
+ }
+
+ private class BindableStarDifficulty : Bindable
+ {
+ public readonly BeatmapInfo Beatmap;
+ public readonly CancellationToken CancellationToken;
+
+ public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken)
+ {
+ Beatmap = beatmap;
+ CancellationToken = cancellationToken;
+ }
+ }
+ }
+
+ public readonly struct StarDifficulty
+ {
+ public readonly double Stars;
+
+ public StarDifficulty(double stars)
+ {
+ Stars = stars;
+
+ // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 2cf3a21975..b4b341634c 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -240,6 +240,9 @@ namespace osu.Game.Beatmaps
beatmapInfo = QueryBeatmap(b => b.ID == info.ID);
}
+ if (beatmapInfo == null)
+ return DefaultBeatmap;
+
lock (workingCache)
{
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
@@ -280,14 +283,16 @@ namespace osu.Game.Beatmaps
/// Returns a list of all usable s.
///
/// A list of available .
- public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All) => GetAllUsableBeatmapSetsEnumerable(includes).ToList();
+ public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) =>
+ GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList();
///
/// Returns a list of all usable s. Note that files are not populated.
///
/// The level of detail to include in the returned objects.
+ /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases.
/// A list of available .
- public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes)
+ public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false)
{
IQueryable queryable;
@@ -309,7 +314,7 @@ namespace osu.Game.Beatmaps
// AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY
// clause which causes queries to take 5-10x longer.
// TODO: remove if upgrading to EF core 3.x.
- return queryable.AsEnumerable().Where(s => !s.DeletePending && !s.Protected);
+ return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected));
}
///
diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
index d47d37806e..16207c7d2a 100644
--- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -48,16 +48,13 @@ namespace osu.Game.Beatmaps
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
- if (api?.State != APIState.Online)
- return Task.CompletedTask;
-
LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}
// todo: expose this when we need to do individual difficulty lookups.
protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken)
- => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler);
+ => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap)
{
@@ -183,6 +180,7 @@ namespace osu.Game.Beatmaps
public void Dispose()
{
cacheDownloadRequest?.Dispose();
+ updateScheduler?.Dispose();
}
[Serializable]
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index af6ca24165..e7788b75f3 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -64,49 +64,49 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The time to find the difficulty control point at.
/// The difficulty control point.
- public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time);
+ public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
///
/// Finds the effect control point that is active at .
///
/// The time to find the effect control point at.
/// The effect control point.
- public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time);
+ public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
///
/// Finds the sound control point that is active at .
///
/// The time to find the sound control point at.
/// The sound control point.
- public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
+ public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
///
/// Finds the timing control point that is active at .
///
/// The time to find the timing control point at.
/// The timing control point.
- public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
+ public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
///
/// Finds the maximum BPM represented by any timing control point.
///
[JsonIgnore]
public double BPMMaximum =>
- 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
+ 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Finds the minimum BPM represented by any timing control point.
///
[JsonIgnore]
public double BPMMinimum =>
- 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
+ 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Finds the mode BPM (most common BPM) represented by the control points.
///
[JsonIgnore]
public double BPMMode =>
- 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
+ 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Remove all s and return to a pristine state.
@@ -170,12 +170,12 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The list to search.
/// The time to find the control point at.
- /// The control point to use when is before any control points. If null, a new control point will be constructed.
+ /// The control point to use when is before any control points.
/// The active control point at , or a fallback if none found.
- private T binarySearchWithFallback(IReadOnlyList list, double time, T prePoint = null)
- where T : ControlPoint, new()
+ private T binarySearchWithFallback(IReadOnlyList list, double time, T fallback)
+ where T : ControlPoint
{
- return binarySearch(list, time) ?? prePoint ?? new T();
+ return binarySearch(list, time) ?? fallback;
}
///
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 2448b2b25c..1d38790f87 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -7,6 +7,11 @@ namespace osu.Game.Beatmaps.ControlPoints
{
public class DifficultyControlPoint : ControlPoint
{
+ public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint
+ {
+ SpeedMultiplierBindable = { Disabled = true },
+ };
+
///
/// The speed multiplier at this control point.
///
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 9b69147468..9e8e3978be 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -7,6 +7,12 @@ namespace osu.Game.Beatmaps.ControlPoints
{
public class EffectControlPoint : ControlPoint
{
+ public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
+ {
+ KiaiModeBindable = { Disabled = true },
+ OmitFirstBarLineBindable = { Disabled = true }
+ };
+
///
/// Whether the first bar line of this control point is ignored.
///
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 61851a00d7..c052c04ea0 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -10,6 +10,12 @@ namespace osu.Game.Beatmaps.ControlPoints
{
public const string DEFAULT_BANK = "normal";
+ public static readonly SampleControlPoint DEFAULT = new SampleControlPoint
+ {
+ SampleBankBindable = { Disabled = true },
+ SampleVolumeBindable = { Disabled = true }
+ };
+
///
/// The default sample bank at this control point.
///
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 1927dd6575..9345299c3a 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -13,6 +13,21 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
+ ///
+ /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
+ ///
+ private const double default_beat_length = 60000.0 / 60.0;
+
+ public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
+ {
+ BeatLengthBindable =
+ {
+ Value = default_beat_length,
+ Disabled = true
+ },
+ TimeSignatureBindable = { Disabled = true }
+ };
+
///
/// The time signature at this control point.
///
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index be5cd78dc8..b30ec0ca2c 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -369,7 +369,9 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, controlPoint, true);
}
- addControlPoint(time, new LegacyDifficultyControlPoint
+#pragma warning disable 618
+ addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
+#pragma warning restore 618
{
SpeedMultiplier = speedMultiplier,
}, timingChange);
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index cefb47893c..57555cce90 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case 2:
- position.X = ((IHasXPosition)hitObject).X * 512;
+ position.X = ((IHasXPosition)hitObject).X;
break;
case 3:
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index a0e83554a3..44ef9bcacc 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -159,11 +159,20 @@ namespace osu.Game.Beatmaps.Formats
Mania,
}
- internal class LegacyDifficultyControlPoint : DifficultyControlPoint
+ [Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")]
+ public class LegacyDifficultyControlPoint : DifficultyControlPoint
{
- public LegacyDifficultyControlPoint()
+ ///
+ /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
+ /// DO NOT USE THIS UNLESS 100% SURE.
+ ///
+ public readonly float BpmMultiplier;
+
+ public LegacyDifficultyControlPoint(double beatLength)
{
SpeedMultiplierBindable.Precision = double.Epsilon;
+
+ BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1;
}
}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 9d31bc9bba..a8a8794320 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -91,6 +91,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
Set(OsuSetting.KeyOverlay, false);
Set(OsuSetting.PositionalHitSounds, true);
+ Set(OsuSetting.AlwaysPlayFirstComboBreak, true);
Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
Set(OsuSetting.FloatingComments, false);
@@ -98,6 +99,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
Set(OsuSetting.IncreaseFirstObjectVisibility, true);
+ Set(OsuSetting.GameplayDisableWinKey, true);
// Update
Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
@@ -180,6 +182,7 @@ namespace osu.Game.Configuration
ShowStoryboard,
KeyOverlay,
PositionalHitSounds,
+ AlwaysPlayFirstComboBreak,
ScoreMeter,
FloatingComments,
ShowInterface,
@@ -227,6 +230,7 @@ namespace osu.Game.Configuration
IntroSequence,
UIHoldActivationDelay,
HitLighting,
- MenuBackgroundSource
+ MenuBackgroundSource,
+ GameplayDisableWinKey
}
}
diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
index dd5c41285a..df063f57d5 100644
--- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
+++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
@@ -43,14 +43,6 @@ namespace osu.Game.Graphics.Containers
///
public double MinimumBeatLength { get; set; }
- ///
- /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
- ///
- private const double default_beat_length = 60000.0 / 60.0;
-
- private TimingControlPoint defaultTiming;
- private EffectControlPoint defaultEffect;
-
protected bool IsBeatSyncedWithTrack { get; private set; }
protected override void Update()
@@ -81,8 +73,8 @@ namespace osu.Game.Graphics.Containers
if (timingPoint == null || !IsBeatSyncedWithTrack)
{
currentTrackTime = Clock.CurrentTime;
- timingPoint = defaultTiming;
- effectPoint = defaultEffect;
+ timingPoint = TimingControlPoint.DEFAULT;
+ effectPoint = EffectControlPoint.DEFAULT;
}
double beatLength = timingPoint.BeatLength / Divisor;
@@ -116,17 +108,6 @@ namespace osu.Game.Graphics.Containers
private void load(IBindable beatmap)
{
Beatmap.BindTo(beatmap);
-
- defaultTiming = new TimingControlPoint
- {
- BeatLength = default_beat_length,
- };
-
- defaultEffect = new EffectControlPoint
- {
- KiaiMode = false,
- OmitFirstBarLine = false
- };
}
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
index b7ea1ba56a..3015c44613 100644
--- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -55,7 +54,16 @@ namespace osu.Game.Graphics.Cursor
return;
}
- var newTarget = inputManager.HoveredDrawables.OfType().FirstOrDefault(t => t.ProvidingUserCursor) ?? this;
+ IProvideCursor newTarget = this;
+
+ foreach (var d in inputManager.HoveredDrawables)
+ {
+ if (d is IProvideCursor p && p.ProvidingUserCursor)
+ {
+ newTarget = p;
+ break;
+ }
+ }
if (currentTarget == newTarget)
return;
diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs
new file mode 100644
index 0000000000..67fcab43f7
--- /dev/null
+++ b/osu.Game/Graphics/DateTooltip.cs
@@ -0,0 +1,78 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+
+namespace osu.Game.Graphics
+{
+ public class DateTooltip : VisibilityContainer, ITooltip
+ {
+ private readonly OsuSpriteText dateText, timeText;
+ private readonly Box background;
+
+ public DateTooltip()
+ {
+ AutoSizeAxes = Axes.Both;
+ Masking = true;
+ CornerRadius = 5;
+
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Padding = new MarginPadding(10),
+ Children = new Drawable[]
+ {
+ dateText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ },
+ timeText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ }
+ }
+ },
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ background.Colour = colours.GreySeafoamDarker;
+ timeText.Colour = colours.BlueLighter;
+ }
+
+ protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
+ protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
+
+ public bool SetContent(object content)
+ {
+ if (!(content is DateTimeOffset date))
+ return false;
+
+ dateText.Text = $"{date:d MMMM yyyy} ";
+ timeText.Text = $"{date:HH:mm:ss \"UTC\"z}";
+ return true;
+ }
+
+ public void Move(Vector2 pos) => Position = pos;
+ }
+}
diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs
index 8b6df4a834..259d9c8d6e 100644
--- a/osu.Game/Graphics/DrawableDate.cs
+++ b/osu.Game/Graphics/DrawableDate.cs
@@ -4,12 +4,9 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
-using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using osu.Game.Utils;
-using osuTK;
namespace osu.Game.Graphics
{
@@ -81,69 +78,5 @@ namespace osu.Game.Graphics
public ITooltip GetCustomTooltip() => new DateTooltip();
public object TooltipContent => Date;
-
- private class DateTooltip : VisibilityContainer, ITooltip
- {
- private readonly OsuSpriteText dateText, timeText;
- private readonly Box background;
-
- public DateTooltip()
- {
- AutoSizeAxes = Axes.Both;
- Masking = true;
- CornerRadius = 5;
-
- Children = new Drawable[]
- {
- background = new Box
- {
- RelativeSizeAxes = Axes.Both
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Padding = new MarginPadding(10),
- Children = new Drawable[]
- {
- dateText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- },
- timeText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
- }
- }
- },
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- background.Colour = colours.GreySeafoamDarker;
- timeText.Colour = colours.BlueLighter;
- }
-
- protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
- protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
-
- public bool SetContent(object content)
- {
- if (!(content is DateTimeOffset date))
- return false;
-
- dateText.Text = $"{date:d MMMM yyyy} ";
- timeText.Text = $"{date:HH:mm:ss \"UTC\"z}";
- return true;
- }
-
- public void Move(Vector2 pos) => Position = pos;
- }
}
}
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index 9804aefce8..d1f6fd445e 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -19,6 +19,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Jpeg;
namespace osu.Game.Graphics
{
@@ -119,7 +120,9 @@ namespace osu.Game.Graphics
break;
case ScreenshotFormat.Jpg:
- image.SaveAsJpeg(stream);
+ const int jpeg_quality = 92;
+
+ image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality });
break;
default:
diff --git a/osu.Game/Graphics/Sprites/LogoAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs
new file mode 100644
index 0000000000..b1383065fe
--- /dev/null
+++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs
@@ -0,0 +1,69 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Vertices;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+
+namespace osu.Game.Graphics.Sprites
+{
+ public class LogoAnimation : Sprite
+ {
+ [BackgroundDependencyLoader]
+ private void load(ShaderManager shaders, TextureStore textures)
+ {
+ TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation");
+ RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); // Masking isn't supported for now
+ }
+
+ private float animationProgress;
+
+ public float AnimationProgress
+ {
+ get => animationProgress;
+ set
+ {
+ if (animationProgress == value) return;
+
+ animationProgress = value;
+ Invalidate(Invalidation.DrawInfo);
+ }
+ }
+
+ public override bool IsPresent => true;
+
+ protected override DrawNode CreateDrawNode() => new LogoAnimationDrawNode(this);
+
+ private class LogoAnimationDrawNode : SpriteDrawNode
+ {
+ private LogoAnimation source => (LogoAnimation)Source;
+
+ private float progress;
+
+ public LogoAnimationDrawNode(LogoAnimation source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ progress = source.animationProgress;
+ }
+
+ protected override void Blit(Action vertexAction)
+ {
+ Shader.GetUniform("progress").UpdateValue(ref progress);
+
+ base.Blit(vertexAction);
+ }
+
+ protected override bool CanDrawOpaqueInterior => false;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
index 84429bf5bd..fb5ff4aad3 100644
--- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
+++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs
@@ -27,6 +27,8 @@ namespace osu.Game.Graphics.UserInterface
{
Height = 32;
TabContainer.Spacing = new Vector2(padding, 0f);
+ SwitchTabOnRemove = false;
+
Current.ValueChanged += index =>
{
foreach (var t in TabContainer.Children.OfType())
diff --git a/osu.Game/Graphics/UserInterface/DownloadButton.cs b/osu.Game/Graphics/UserInterface/DownloadButton.cs
index 86a5cb9aa6..da6c95299e 100644
--- a/osu.Game/Graphics/UserInterface/DownloadButton.cs
+++ b/osu.Game/Graphics/UserInterface/DownloadButton.cs
@@ -63,16 +63,19 @@ namespace osu.Game.Graphics.UserInterface
background.FadeColour(colours.Gray4, 500, Easing.InOutExpo);
icon.MoveToX(0, 500, Easing.InOutExpo);
checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo);
+ TooltipText = "Download";
break;
case DownloadState.Downloading:
background.FadeColour(colours.Blue, 500, Easing.InOutExpo);
icon.MoveToX(0, 500, Easing.InOutExpo);
checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo);
+ TooltipText = "Downloading...";
break;
case DownloadState.Downloaded:
background.FadeColour(colours.Yellow, 500, Easing.InOutExpo);
+ TooltipText = "Importing";
break;
case DownloadState.LocallyAvailable:
diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
index 8977f014b6..f77a3109c9 100644
--- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
@@ -67,6 +67,8 @@ namespace osu.Game.Graphics.UserInterface
public bool OnPressed(GlobalAction action)
{
+ if (!HasFocus) return false;
+
if (action == GlobalAction.Back)
{
if (Text.Length > 0)
diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
index 0c82a869f8..ac6f5ceb1b 100644
--- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs
@@ -24,6 +24,8 @@ namespace osu.Game.Graphics.UserInterface
Child = new PasswordMaskChar(CalculatedTextSize),
};
+ protected override bool AllowUniqueCharacterSamples => false;
+
protected override bool AllowClipboardExport => false;
private readonly CapsWarning warning;
diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
index c2feca171b..61501b0cd8 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Graphics.UserInterface
{
private Color4 accentColour;
+ public const float HORIZONTAL_SPACING = 10;
+
public virtual Color4 AccentColour
{
get => accentColour;
@@ -54,7 +56,7 @@ namespace osu.Game.Graphics.UserInterface
public OsuTabControl()
{
- TabContainer.Spacing = new Vector2(10f, 0f);
+ TabContainer.Spacing = new Vector2(HORIZONTAL_SPACING, 0f);
AddInternal(strip = new Box
{
diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
index 544acc7eb2..bdc95ee048 100644
--- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs
@@ -21,7 +21,6 @@ namespace osu.Game.Graphics.UserInterface
{
private readonly Box box;
private readonly SpriteText text;
- private readonly SpriteIcon icon;
private Color4? accentColour;
@@ -32,12 +31,6 @@ namespace osu.Game.Graphics.UserInterface
{
accentColour = value;
- if (Current.Value)
- {
- text.Colour = AccentColour;
- icon.Colour = AccentColour;
- }
-
updateFade();
}
}
@@ -52,6 +45,8 @@ namespace osu.Game.Graphics.UserInterface
public OsuTabControlCheckbox()
{
+ SpriteIcon icon;
+
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
@@ -89,6 +84,8 @@ namespace osu.Game.Graphics.UserInterface
{
icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle;
text.Font = text.Font.With(weight: selected.NewValue ? FontWeight.Bold : FontWeight.Medium);
+
+ updateFade();
};
}
@@ -115,8 +112,8 @@ namespace osu.Game.Graphics.UserInterface
private void updateFade()
{
- box.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
- text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint);
+ box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
+ text.FadeColour(Current.Value || IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint);
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
index 06c46fbb91..0d173e2d3e 100644
--- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
@@ -1,7 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
@@ -11,6 +14,7 @@ using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osuTK;
@@ -19,6 +23,18 @@ namespace osu.Game.Graphics.UserInterface
{
public class OsuTextBox : BasicTextBox
{
+ private readonly SampleChannel[] textAddedSamples = new SampleChannel[4];
+ private SampleChannel capsTextAddedSample;
+ private SampleChannel textRemovedSample;
+ private SampleChannel textCommittedSample;
+ private SampleChannel caretMovedSample;
+
+ ///
+ /// Whether to allow playing a different samples based on the type of character.
+ /// If set to false, the same sample will be used for all characters.
+ ///
+ protected virtual bool AllowUniqueCharacterSamples => true;
+
protected override float LeftRightPadding => 10;
protected override float CaretWidth => 3;
@@ -41,15 +57,54 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader]
- private void load(OsuColour colour)
+ private void load(OsuColour colour, AudioManager audio)
{
BackgroundUnfocused = Color4.Black.Opacity(0.5f);
BackgroundFocused = OsuColour.Gray(0.3f).Opacity(0.8f);
BackgroundCommit = BorderColour = colour.Yellow;
+
+ for (int i = 0; i < textAddedSamples.Length; i++)
+ textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}");
+
+ capsTextAddedSample = audio.Samples.Get(@"Keyboard/key-caps");
+ textRemovedSample = audio.Samples.Get(@"Keyboard/key-delete");
+ textCommittedSample = audio.Samples.Get(@"Keyboard/key-confirm");
+ caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement");
}
protected override Color4 SelectionColour => new Color4(249, 90, 255, 255);
+ protected override void OnTextAdded(string added)
+ {
+ base.OnTextAdded(added);
+
+ if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples)
+ capsTextAddedSample?.Play();
+ else
+ textAddedSamples[RNG.Next(0, 3)]?.Play();
+ }
+
+ protected override void OnTextRemoved(string removed)
+ {
+ base.OnTextRemoved(removed);
+
+ textRemovedSample?.Play();
+ }
+
+ protected override void OnTextCommitted(bool textChanged)
+ {
+ base.OnTextCommitted(textChanged);
+
+ textCommittedSample?.Play();
+ }
+
+ protected override void OnCaretMoved(bool selecting)
+ {
+ base.OnCaretMoved(selecting);
+
+ caretMovedSample?.Play();
+ }
+
protected override void OnFocus(FocusEvent e)
{
BorderThickness = 3;
diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
index c9cd9f1158..924c7913f3 100644
--- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
+++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
@@ -1,13 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
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.Graphics.Sprites;
+using osu.Game.Overlays;
using osuTK;
-using osuTK.Graphics;
using System.Collections.Generic;
namespace osu.Game.Graphics.UserInterface
@@ -16,14 +18,6 @@ namespace osu.Game.Graphics.UserInterface
{
private const int duration = 200;
- private Color4 chevronIconColour;
-
- protected Color4 ChevronIconColour
- {
- get => chevronIconColour;
- set => chevronIconColour = leftChevron.Colour = rightChevron.Colour = value;
- }
-
public string Text
{
get => text.Text;
@@ -32,22 +26,28 @@ namespace osu.Game.Graphics.UserInterface
protected override IEnumerable EffectTargets => new[] { background };
- private ChevronIcon leftChevron;
- private ChevronIcon rightChevron;
+ private ChevronIcon leftIcon;
+ private ChevronIcon rightIcon;
private SpriteText text;
private Box background;
private FillFlowContainer textContainer;
public ShowMoreButton()
{
- Height = 30;
- Width = 140;
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ IdleColour = colourProvider.Background2;
+ HoverColour = colourProvider.Background1;
}
protected override Drawable CreateContent() => new CircularContainer
{
Masking = true,
- RelativeSizeAxes = Axes.Both,
+ AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
@@ -56,22 +56,36 @@ namespace osu.Game.Graphics.UserInterface
},
textContainer = new FillFlowContainer
{
+ AlwaysPresent = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(7),
+ Spacing = new Vector2(10),
+ Margin = new MarginPadding
+ {
+ Horizontal = 20,
+ Vertical = 5
+ },
Children = new Drawable[]
{
- leftChevron = new ChevronIcon(),
+ leftIcon = new ChevronIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = "show more".ToUpper(),
},
- rightChevron = new ChevronIcon(),
+ rightIcon = new ChevronIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
}
}
}
@@ -81,17 +95,40 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnLoadFinished() => textContainer.FadeIn(duration, Easing.OutQuint);
- private class ChevronIcon : SpriteIcon
+ protected override bool OnHover(HoverEvent e)
{
- private const int icon_size = 8;
+ base.OnHover(e);
+ leftIcon.SetHoveredState(true);
+ rightIcon.SetHoveredState(true);
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+ leftIcon.SetHoveredState(false);
+ rightIcon.SetHoveredState(false);
+ }
+
+ public class ChevronIcon : SpriteIcon
+ {
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
public ChevronIcon()
{
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
- Size = new Vector2(icon_size);
+ Size = new Vector2(7.5f);
Icon = FontAwesome.Solid.ChevronDown;
}
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Colour = colourProvider.Foreground1;
+ }
+
+ public void SetHoveredState(bool hovered) =>
+ this.FadeColour(hovered ? colourProvider.Light1 : colourProvider.Foreground1, 200, Easing.OutQuint);
}
}
}
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 7104031b56..8097f61ea4 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -1,6 +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.Diagnostics;
+using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Configuration;
@@ -9,7 +12,26 @@ namespace osu.Game.IO
{
public class OsuStorage : MigratableStorage
{
+ ///
+ /// Indicates the error (if any) that occurred when initialising the custom storage during initial startup.
+ ///
+ public readonly OsuStorageError Error;
+
+ ///
+ /// The custom storage path as selected by the user.
+ ///
+ [CanBeNull]
+ public string CustomStoragePath => storageConfig.Get(StorageConfig.FullPath);
+
+ ///
+ /// The default storage path to be used if a custom storage path hasn't been selected or is not accessible.
+ ///
+ [NotNull]
+ public string DefaultStoragePath => defaultStorage.GetFullPath(".");
+
+ private readonly GameHost host;
private readonly StorageConfigManager storageConfig;
+ private readonly Storage defaultStorage;
public override string[] IgnoreDirectories => new[] { "cache" };
@@ -19,15 +41,57 @@ namespace osu.Game.IO
"storage.ini"
};
- public OsuStorage(GameHost host)
- : base(host.Storage, string.Empty)
+ public OsuStorage(GameHost host, Storage defaultStorage)
+ : base(defaultStorage, string.Empty)
{
- storageConfig = new StorageConfigManager(host.Storage);
+ this.host = host;
+ this.defaultStorage = defaultStorage;
- var customStoragePath = storageConfig.Get(StorageConfig.FullPath);
+ storageConfig = new StorageConfigManager(defaultStorage);
- if (!string.IsNullOrEmpty(customStoragePath))
- ChangeTargetStorage(host.GetStorage(customStoragePath));
+ if (!string.IsNullOrEmpty(CustomStoragePath))
+ TryChangeToCustomStorage(out Error);
+ }
+
+ ///
+ /// Resets the custom storage path, changing the target storage to the default location.
+ ///
+ public void ResetCustomStoragePath()
+ {
+ storageConfig.Set(StorageConfig.FullPath, string.Empty);
+ storageConfig.Save();
+
+ ChangeTargetStorage(defaultStorage);
+ }
+
+ ///
+ /// Attempts to change to the user's custom storage path.
+ ///
+ /// The error that occurred.
+ /// Whether the custom storage path was used successfully. If not, will be populated with the reason.
+ public bool TryChangeToCustomStorage(out OsuStorageError error)
+ {
+ Debug.Assert(!string.IsNullOrEmpty(CustomStoragePath));
+
+ error = OsuStorageError.None;
+ Storage lastStorage = UnderlyingStorage;
+
+ try
+ {
+ Storage userStorage = host.GetStorage(CustomStoragePath);
+
+ if (!userStorage.ExistsDirectory(".") || !userStorage.GetFiles(".").Any())
+ error = OsuStorageError.AccessibleButEmpty;
+
+ ChangeTargetStorage(userStorage);
+ }
+ catch
+ {
+ error = OsuStorageError.NotAccessible;
+ ChangeTargetStorage(lastStorage);
+ }
+
+ return error == OsuStorageError.None;
}
protected override void ChangeTargetStorage(Storage newStorage)
@@ -43,4 +107,23 @@ namespace osu.Game.IO
storageConfig.Save();
}
}
+
+ public enum OsuStorageError
+ {
+ ///
+ /// No error.
+ ///
+ None,
+
+ ///
+ /// Occurs when the target storage directory is accessible but does not already contain game files.
+ /// Only happens when the user changes the storage directory and then moves the files manually or mounts a different device to the same path.
+ ///
+ AccessibleButEmpty,
+
+ ///
+ /// Occurs when the target storage directory cannot be accessed at all.
+ ///
+ NotAccessible,
+ }
}
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 618798a6d8..6ae420b162 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleDirect),
+ new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
@@ -56,6 +57,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
+ new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
};
public IEnumerable AudioControlKeyBindings => new[]
@@ -157,5 +159,11 @@ namespace osu.Game.Input.Bindings
[Description("Home")]
Home,
+
+ [Description("Toggle notifications")]
+ ToggleNotifications,
+
+ [Description("Pause")]
+ PauseGameplay,
}
}
diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs
index 74b3134964..198ab6883d 100644
--- a/osu.Game/Input/KeyBindingStore.cs
+++ b/osu.Game/Input/KeyBindingStore.cs
@@ -55,6 +55,9 @@ namespace osu.Game.Input
RulesetID = rulesetId,
Variant = variant
});
+
+ // required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686)
+ usage.Context.SaveChanges();
}
}
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 0f8acbb7af..6912d9b629 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -5,6 +5,7 @@ using System;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
+using osu.Game.Users;
namespace osu.Game.Online.API
{
@@ -61,6 +62,11 @@ namespace osu.Game.Online.API
protected APIAccess API;
protected WebRequest WebRequest;
+ ///
+ /// The currently logged in user. Note that this will only be populated during