diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index bc285dbe11..011a37cbdc 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -9,7 +9,6 @@
false
-
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 718ada1905..c04f6132f3 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,7 +9,6 @@
false
-
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index 6b9c3f4d63..529054fd4f 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -9,7 +9,6 @@
false
-
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 718ada1905..c04f6132f3 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,7 +9,6 @@
false
-
diff --git a/osu.Android.props b/osu.Android.props
index 8c15ed7949..caaa83bff4 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 712f300671..cebbcb40b7 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -14,6 +14,7 @@ using osu.Framework.Platform;
using osu.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
+using SDL2;
using Squirrel;
namespace osu.Desktop
@@ -29,7 +30,21 @@ namespace osu.Desktop
{
// run Squirrel first, as the app may exit after these run
if (OperatingSystem.IsWindows())
+ {
+ var windowsVersion = Environment.OSVersion.Version;
+
+ // While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
+ // See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
+ if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
+ {
+ SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
+ "Your operating system is too old to run osu!",
+ "This version of osu! requires at least Windows 8.1 to run.\nPlease upgrade your operating system or consider using an older version of osu!.", IntPtr.Zero);
+ return;
+ }
+
setupSquirrel();
+ }
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index a4f9e2671b..c67017f175 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
index 07ffda4030..1d207d04c7 100644
--- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
+++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.IO;
using BenchmarkDotNet.Attributes;
using osu.Framework.IO.Stores;
diff --git a/osu.Game.Benchmarks/BenchmarkMod.cs b/osu.Game.Benchmarks/BenchmarkMod.cs
index a1d92d9a67..994300df36 100644
--- a/osu.Game.Benchmarks/BenchmarkMod.cs
+++ b/osu.Game.Benchmarks/BenchmarkMod.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Osu.Mods;
@@ -11,7 +9,7 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkMod : BenchmarkTest
{
- private OsuModDoubleTime mod;
+ private OsuModDoubleTime mod = null!;
[Params(1, 10, 100)]
public int Times { get; set; }
diff --git a/osu.Game.Benchmarks/BenchmarkRealmReads.cs b/osu.Game.Benchmarks/BenchmarkRealmReads.cs
index 5ffda6504e..1df77320d2 100644
--- a/osu.Game.Benchmarks/BenchmarkRealmReads.cs
+++ b/osu.Game.Benchmarks/BenchmarkRealmReads.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
using System.Threading;
using BenchmarkDotNet.Attributes;
@@ -17,9 +15,9 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkRealmReads : BenchmarkTest
{
- private TemporaryNativeStorage storage;
- private RealmAccess realm;
- private UpdateThread updateThread;
+ private TemporaryNativeStorage storage = null!;
+ private RealmAccess realm = null!;
+ private UpdateThread updateThread = null!;
[Params(1, 100, 1000)]
public int ReadsPerFetch { get; set; }
@@ -135,9 +133,9 @@ namespace osu.Game.Benchmarks
[GlobalCleanup]
public void Cleanup()
{
- realm?.Dispose();
- storage?.Dispose();
- updateThread?.Exit();
+ realm.Dispose();
+ storage.Dispose();
+ updateThread.Exit();
}
}
}
diff --git a/osu.Game.Benchmarks/BenchmarkRuleset.cs b/osu.Game.Benchmarks/BenchmarkRuleset.cs
index de8cb13773..7d318e043b 100644
--- a/osu.Game.Benchmarks/BenchmarkRuleset.cs
+++ b/osu.Game.Benchmarks/BenchmarkRuleset.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using osu.Game.Online.API;
@@ -13,9 +11,9 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkRuleset : BenchmarkTest
{
- private OsuRuleset ruleset;
- private APIMod apiModDoubleTime;
- private APIMod apiModDifficultyAdjust;
+ private OsuRuleset ruleset = null!;
+ private APIMod apiModDoubleTime = null!;
+ private APIMod apiModDifficultyAdjust = null!;
public override void SetUp()
{
diff --git a/osu.Game.Benchmarks/BenchmarkTest.cs b/osu.Game.Benchmarks/BenchmarkTest.cs
index 140696e4a4..34f5edd084 100644
--- a/osu.Game.Benchmarks/BenchmarkTest.cs
+++ b/osu.Game.Benchmarks/BenchmarkTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using NUnit.Framework;
diff --git a/osu.Game.Benchmarks/Program.cs b/osu.Game.Benchmarks/Program.cs
index 603d8aa1b9..439ced53ab 100644
--- a/osu.Game.Benchmarks/Program.cs
+++ b/osu.Game.Benchmarks/Program.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
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 b957ade952..3ac1491946 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -1,7 +1,6 @@
-
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 d3b4b378c0..d07df75864 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -1,7 +1,6 @@
-
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.cs
new file mode 100644
index 0000000000..6bd41e2fa5
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.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 NUnit.Framework;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModRepel : OsuModTestScene
+ {
+ [TestCase(0.1f)]
+ [TestCase(0.5f)]
+ [TestCase(1)]
+ public void TestRepel(float strength)
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModRepel
+ {
+ RepulsionStrength = { Value = strength },
+ },
+ PassCondition = () => true,
+ Autoplay = false,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
new file mode 100644
index 0000000000..1aed84be10
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
@@ -0,0 +1,175 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModSingleTap : OsuModTestScene
+ {
+ [Test]
+ public void TestInputSingular() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 1500,
+ Position = new Vector2(300, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 2000,
+ Position = new Vector2(400, 100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
+ }
+ });
+
+ [Test]
+ public void TestInputAlternating() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
+ new OsuReplayFrame(1001, new Vector2(200, 100)),
+ new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(1501, new Vector2(300, 100)),
+ new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
+ new OsuReplayFrame(2001, new Vector2(400, 100)),
+ }
+ });
+
+ ///
+ /// Ensures singletapping is reset before the first hitobject after intro.
+ ///
+ [Test]
+ public void TestInputAlternatingAtIntro() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ // first press during intro.
+ new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(200)),
+ // press different key at hitobject and ensure it has been hit.
+ new OsuReplayFrame(1000, new Vector2(100), OsuAction.RightButton),
+ }
+ });
+
+ ///
+ /// Ensures singletapping is reset before the first hitobject after a break.
+ ///
+ [Test]
+ public void TestInputAlternatingWithBreak() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ Breaks = new List
+ {
+ new BreakPeriod(500, 2000),
+ },
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 2500,
+ Position = new Vector2(500, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(500, 100),
+ },
+ }
+ },
+ ReplayFrames = new List
+ {
+ // first press to start singletap lock.
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ // press different key after break but before hit object.
+ new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.RightButton),
+ new OsuReplayFrame(2251, new Vector2(300, 100)),
+ // press same key at second hitobject and ensure it has been hit.
+ new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2501, new Vector2(500, 100)),
+ // press different key at third hitobject and ensure it has been missed.
+ new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.RightButton),
+ new OsuReplayFrame(3001, new Vector2(500, 100)),
+ }
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 2c0d3fd937..4349d25cb3 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -1,9 +1,8 @@
-
-
+
diff --git a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
new file mode 100644
index 0000000000..a7aca8257b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.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 System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
+using osu.Game.Utils;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset
+ {
+ public override double ScoreMultiplier => 1.0;
+ public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
+ public override ModType Type => ModType.Conversion;
+
+ private const double flash_duration = 1000;
+
+ private DrawableRuleset ruleset = null!;
+
+ protected OsuAction? LastAcceptedAction { get; private set; }
+
+ ///
+ /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
+ ///
+ ///
+ /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time.
+ ///
+ private PeriodTracker nonGameplayPeriods = null!;
+
+ private IFrameStableClock gameplayClock = null!;
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ ruleset = drawableRuleset;
+ drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
+
+ var periods = new List();
+
+ if (drawableRuleset.Objects.Any())
+ {
+ periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
+
+ foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
+ periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
+
+ static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
+ }
+
+ nonGameplayPeriods = new PeriodTracker(periods);
+
+ gameplayClock = drawableRuleset.FrameStableClock;
+ }
+
+ protected abstract bool CheckValidNewAction(OsuAction action);
+
+ private bool checkCorrectAction(OsuAction action)
+ {
+ if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
+ {
+ LastAcceptedAction = null;
+ return true;
+ }
+
+ switch (action)
+ {
+ case OsuAction.LeftButton:
+ case OsuAction.RightButton:
+ break;
+
+ // Any action which is not left or right button should be ignored.
+ default:
+ return true;
+ }
+
+ if (CheckValidNewAction(action))
+ {
+ LastAcceptedAction = action;
+ return true;
+ }
+
+ ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
+ return false;
+ }
+
+ private class InputInterceptor : Component, IKeyBindingHandler
+ {
+ private readonly InputBlockingMod mod;
+
+ public InputInterceptor(InputBlockingMod mod)
+ {
+ this.mod = mod;
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ // if the pressed action is incorrect, block it from reaching gameplay.
+ => !mod.checkCorrectAction(e.Action);
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
index 622d2df432..d88cb17e84 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
@@ -1,119 +1,20 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
-using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Input.Events;
-using osu.Game.Beatmaps.Timing;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Play;
-using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModAlternate : Mod, IApplicableToDrawableRuleset
+ public class OsuModAlternate : InputBlockingMod
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
- public override double ScoreMultiplier => 1.0;
- public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
- public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
- private const double flash_duration = 1000;
-
- ///
- /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
- ///
- ///
- /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time.
- ///
- private PeriodTracker nonGameplayPeriods;
-
- private OsuAction? lastActionPressed;
- private DrawableRuleset ruleset;
-
- private IFrameStableClock gameplayClock;
-
- public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
- {
- ruleset = drawableRuleset;
- drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
-
- var periods = new List();
-
- if (drawableRuleset.Objects.Any())
- {
- periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
-
- foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
- periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
-
- static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
- }
-
- nonGameplayPeriods = new PeriodTracker(periods);
-
- gameplayClock = drawableRuleset.FrameStableClock;
- }
-
- private bool checkCorrectAction(OsuAction action)
- {
- if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
- {
- lastActionPressed = null;
- return true;
- }
-
- switch (action)
- {
- case OsuAction.LeftButton:
- case OsuAction.RightButton:
- break;
-
- // Any action which is not left or right button should be ignored.
- default:
- return true;
- }
-
- if (lastActionPressed != action)
- {
- // User alternated correctly.
- lastActionPressed = action;
- return true;
- }
-
- ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
- return false;
- }
-
- private class InputInterceptor : Component, IKeyBindingHandler
- {
- private readonly OsuModAlternate mod;
-
- public InputInterceptor(OsuModAlternate mod)
- {
- this.mod = mod;
- }
-
- public bool OnPressed(KeyBindingPressEvent e)
- // if the pressed action is incorrect, block it from reaching gameplay.
- => !mod.checkCorrectAction(e.Action);
-
- public void OnReleased(KeyBindingReleaseEvent e)
- {
- }
- }
+ protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 4c9418726c..a3f6448457 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public bool PerformFail() => false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index d562c37541..c4de77b8a3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index 656cf95e77..704b922ee5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index cee40866b1..f9a74d2a3a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 2cf8c278ca..2030156f2e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
///
/// How early before a hitobject's start time to trigger a hit.
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
new file mode 100644
index 0000000000..211987ee32
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
@@ -0,0 +1,98 @@
+// 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 osu.Framework.Bindables;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Osu.Utils;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ internal class OsuModRepel : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset
+ {
+ public override string Name => "Repel";
+ public override string Acronym => "RP";
+ public override ModType Type => ModType.Fun;
+ public override string Description => "Hit objects run away!";
+ public override double ScoreMultiplier => 1;
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
+
+ private IFrameStableClock? gameplayClock;
+
+ [SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
+ public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
+ {
+ Precision = 0.05f,
+ MinValue = 0.05f,
+ MaxValue = 1.0f,
+ };
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ gameplayClock = drawableRuleset.FrameStableClock;
+
+ // Hide judgment displays and follow points as they won't make any sense.
+ // Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
+ drawableRuleset.Playfield.DisplayJudgements.Value = false;
+ (drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
+ }
+
+ public void Update(Playfield playfield)
+ {
+ var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
+
+ foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
+ {
+ var destination = Vector2.Clamp(2 * drawable.Position - cursorPos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
+
+ if (drawable.HitObject is Slider thisSlider)
+ {
+ var possibleMovementBounds = OsuHitObjectGenerationUtils.CalculatePossibleMovementBounds(thisSlider);
+
+ destination = Vector2.Clamp(
+ destination,
+ new Vector2(possibleMovementBounds.Left, possibleMovementBounds.Top),
+ new Vector2(possibleMovementBounds.Right, possibleMovementBounds.Bottom)
+ );
+ }
+
+ switch (drawable)
+ {
+ case DrawableHitCircle circle:
+ easeTo(circle, destination, cursorPos);
+ break;
+
+ case DrawableSlider slider:
+
+ if (!slider.HeadCircle.Result.HasResult)
+ easeTo(slider, destination, cursorPos);
+ else
+ easeTo(slider, destination - slider.Ball.DrawPosition, cursorPos);
+
+ break;
+ }
+ }
+ }
+
+ private void easeTo(DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
+ {
+ Debug.Assert(gameplayClock != null);
+
+ double dampLength = Vector2.Distance(hitObject.Position, cursorPos) / (0.04 * RepulsionStrength.Value + 0.04);
+
+ float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
+ float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
+
+ hitObject.Position = new Vector2(x, y);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
new file mode 100644
index 0000000000..051ceb968c
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
@@ -0,0 +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 System;
+using System.Linq;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModSingleTap : InputBlockingMod
+ {
+ public override string Name => @"Single Tap";
+ public override string Acronym => @"ST";
+ public override string Description => @"You must only use one key!";
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
+
+ protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index 5a08df3803..84906f6eed 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private float theta;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index 3fba2cefd2..8acd4fc422 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 91bb7f95f6..d83f5df7a3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -319,13 +319,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
const float fade_out_time = 450;
- // intentionally pile on an extra FadeOut to make it happen much faster.
- Ball.FadeOut(fade_out_time / 4, Easing.Out);
-
switch (state)
{
case ArmedState.Hit:
- Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
index 7bde60b39d..6bfb4e8aae 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
{
+ public const float FOLLOW_AREA = 2.4f;
+
public Func GetInitialHitAction;
public Color4 AccentColour
@@ -31,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
set => ball.Colour = value;
}
- private Drawable followCircle;
private Drawable followCircleReceptor;
private DrawableSlider drawableSlider;
private Drawable ball;
@@ -47,12 +48,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Children = new[]
{
- followCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
+ new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Alpha = 0,
},
followCircleReceptor = new CircularContainer
{
@@ -103,10 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
tracking = value;
- followCircleReceptor.Scale = new Vector2(tracking ? 2.4f : 1f);
-
- followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
- followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
+ followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f);
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 120ce32612..302194e91a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(),
new OsuModRandom(),
new OsuModMirror(),
- new OsuModAlternate(),
+ new MultiMod(new OsuModAlternate(), new OsuModSingleTap())
};
case ModType.Automation:
@@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
- new OsuModMagnetised(),
+ new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed()
};
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
index 8211448705..254e220996 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
@@ -1,19 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
- public class DefaultFollowCircle : CompositeDrawable
+ public class DefaultFollowCircle : FollowCircle
{
public DefaultFollowCircle()
{
- RelativeSizeAxes = Axes.Both;
-
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
@@ -29,5 +29,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
};
}
+
+ protected override void OnTrackingChanged(ValueChangedEvent tracking)
+ {
+ const float scale_duration = 300f;
+ const float fade_duration = 300f;
+
+ this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint)
+ .FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint);
+ }
+
+ protected override void OnSliderEnd()
+ {
+ const float fade_duration = 450f;
+
+ // intentionally pile on an extra FadeOut to make it happen much faster
+ this.FadeOut(fade_duration / 4, Easing.Out);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
index 47308375e6..97bb4a3697 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -19,13 +17,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class DefaultSliderBall : CompositeDrawable
{
- private Box box;
+ private Box box = null!;
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? parentObject { get; set; }
[BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ private void load(ISkinSource skin)
{
- var slider = (DrawableSlider)drawableObject;
-
RelativeSizeAxes = Axes.Both;
float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
@@ -51,10 +50,62 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
};
- slider.Tracking.BindValueChanged(trackingChanged, true);
+ if (parentObject != null)
+ {
+ var slider = (DrawableSlider)parentObject;
+ slider.Tracking.BindValueChanged(trackingChanged, true);
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (parentObject != null)
+ {
+ parentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(parentObject, parentObject.State.Value);
+ }
}
private void trackingChanged(ValueChangedEvent tracking) =>
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ const float fade_duration = 450f;
+
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
+ {
+ this.FadeIn()
+ .ScaleTo(1f);
+ }
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
+ // intentionally pile on an extra FadeOut to make it happen much faster
+ this.FadeOut(fade_duration / 4, Easing.Out);
+
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(1.4f, fade_duration, Easing.Out);
+ break;
+ }
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (parentObject != null)
+ parentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs
new file mode 100644
index 0000000000..321705d25e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs
@@ -0,0 +1,75 @@
+// 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.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public abstract class FollowCircle : CompositeDrawable
+ {
+ [Resolved]
+ protected DrawableHitObject? ParentObject { get; private set; }
+
+ protected FollowCircle()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(OnTrackingChanged, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (ParentObject != null)
+ {
+ ParentObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(ParentObject);
+
+ ParentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(ParentObject, ParentObject.State.Value);
+ }
+ }
+
+ private void onHitObjectApplied(DrawableHitObject drawableObject)
+ {
+ this.ScaleTo(1f)
+ .FadeOut();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ OnSliderEnd();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (ParentObject != null)
+ {
+ ParentObject.HitObjectApplied -= onHitObjectApplied;
+ ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+
+ protected abstract void OnTrackingChanged(ValueChangedEvent tracking);
+
+ protected abstract void OnSliderEnd();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
index b8a559ce07..324f2525bc 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
@@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Diagnostics;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
- public class LegacyFollowCircle : CompositeDrawable
+ public class LegacyFollowCircle : FollowCircle
{
public LegacyFollowCircle(Drawable animationContent)
{
@@ -18,5 +20,36 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
RelativeSizeAxes = Axes.Both;
InternalChild = animationContent;
}
+
+ protected override void OnTrackingChanged(ValueChangedEvent tracking)
+ {
+ Debug.Assert(ParentObject != null);
+
+ if (ParentObject.Judged)
+ return;
+
+ double remainingTime = ParentObject.HitStateUpdateTime - Time.Current;
+
+ // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
+ // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
+ if (tracking.NewValue)
+ {
+ // TODO: Follow circle should bounce on each slider tick.
+ this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
+ .FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
+ }
+ else
+ {
+ // TODO: Should animate only at the next slider tick if we want to match stable perfectly.
+ this.ScaleTo(4f, 100)
+ .FadeTo(0f, 100);
+ }
+ }
+
+ protected override void OnSliderEnd()
+ {
+ this.ScaleTo(1.6f, 200, Easing.Out)
+ .FadeOut(200, Easing.In);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
index 3a156d4d25..a9ae313a31 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -194,7 +194,28 @@ namespace osu.Game.Rulesets.Osu.Utils
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
{
var slider = (Slider)workingObject.HitObject;
- var possibleMovementBounds = calculatePossibleMovementBounds(slider);
+ var possibleMovementBounds = CalculatePossibleMovementBounds(slider);
+
+ // The slider rotation applied in computeModifiedPosition might make it impossible to fit the slider into the playfield
+ // For example, a long horizontal slider will be off-screen when rotated by 90 degrees
+ // In this case, limit the rotation to either 0 or 180 degrees
+ if (possibleMovementBounds.Width < 0 || possibleMovementBounds.Height < 0)
+ {
+ float currentRotation = getSliderRotation(slider);
+ float diff1 = getAngleDifference(workingObject.RotationOriginal, currentRotation);
+ float diff2 = getAngleDifference(workingObject.RotationOriginal + MathF.PI, currentRotation);
+
+ if (diff1 < diff2)
+ {
+ RotateSlider(slider, workingObject.RotationOriginal - getSliderRotation(slider));
+ }
+ else
+ {
+ RotateSlider(slider, workingObject.RotationOriginal + MathF.PI - getSliderRotation(slider));
+ }
+
+ possibleMovementBounds = CalculatePossibleMovementBounds(slider);
+ }
var previousPosition = workingObject.PositionModified;
@@ -239,10 +260,12 @@ namespace osu.Game.Rulesets.Osu.Utils
/// Calculates a which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
///
+ /// The for which to calculate a movement bounding box.
+ /// A which contains all of the possible movements of the slider such that the entire slider is inside the playfield.
///
/// If the slider is larger than the playfield, the returned may have negative width/height.
///
- private static RectangleF calculatePossibleMovementBounds(Slider slider)
+ public static RectangleF CalculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
@@ -353,6 +376,18 @@ namespace osu.Game.Rulesets.Osu.Utils
return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
}
+ ///
+ /// Get the absolute difference between 2 angles measured in Radians.
+ ///
+ /// The first angle
+ /// The second angle
+ /// The absolute difference with interval [0, MathF.PI)
+ private static float getAngleDifference(float angle1, float angle2)
+ {
+ float diff = MathF.Abs(angle1 - angle2) % (MathF.PI * 2);
+ return MathF.Min(diff, MathF.PI * 2 - diff);
+ }
+
public class ObjectPositionInfo
{
///
@@ -395,6 +430,7 @@ namespace osu.Game.Rulesets.Osu.Utils
private class WorkingObject
{
+ public float RotationOriginal { get; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionModified { get; set; }
public Vector2 EndPositionModified { get; set; }
@@ -405,6 +441,7 @@ namespace osu.Game.Rulesets.Osu.Utils
public WorkingObject(ObjectPositionInfo positionInfo)
{
PositionInfo = positionInfo;
+ RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
PositionModified = PositionOriginal = HitObject.Position;
EndPositionModified = HitObject.EndPosition;
}
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 ce468d399b..51d4bbc630 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -1,7 +1,6 @@
-
diff --git a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
index de23b012c1..c887105da6 100644
--- a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
+++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
@@ -4,9 +4,12 @@
#nullable disable
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
+using osu.Game.Models;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.NonVisual
{
@@ -23,6 +26,47 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
}
+ [Test]
+ public void TestAudioEqualityNoFile()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ [Test]
+ public void TestAudioEqualitySameHash()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ addAudioFile(beatmapSetA, "abc");
+ addAudioFile(beatmapSetB, "abc");
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ [Test]
+ public void TestAudioEqualityDifferentHash()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ addAudioFile(beatmapSetA);
+ addAudioFile(beatmapSetB);
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ private static void addAudioFile(BeatmapSetInfo beatmapSetInfo, string hash = null)
+ {
+ beatmapSetInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = hash ?? Guid.NewGuid().ToString() }, "audio.mp3"));
+ }
+
[Test]
public void TestDatabasedWithDatabased()
{
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 91d4eb70e8..41404b2636 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -134,6 +134,7 @@ namespace osu.Game.Tests.Resources
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
StarRating = diff,
Length = length,
+ BeatmapSet = beatmapSet,
BPM = bpm,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
Ruleset = rulesetInfo,
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index f565ca3ef4..6ad6f0b299 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Screens;
@@ -103,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
*/
public void TestAddAudioTrack()
{
+ AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
+
AddAssert("switch track to real track", () =>
{
var setup = Editor.ChildrenOfType().First();
@@ -131,6 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
}
});
+ AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index 8e325838ff..d7e9cc1bc0 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -6,11 +6,13 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Select;
@@ -23,7 +25,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestCantExitWithoutSaving()
{
+ AddUntilStep("Wait for dialog overlay load", () => ((Drawable)Game.Dependencies.Get()).IsLoaded);
AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
+ AddAssert("Sample playback disabled", () => Editor.SamplePlaybackDisabled.Value);
AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
new file mode 100644
index 0000000000..5ec9e88728
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
@@ -0,0 +1,122 @@
+// 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.Audio;
+using osu.Framework.Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Platform;
+using osu.Framework.Screens;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Ranking;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestScenePlayerLocalScoreImport : PlayerTestScene
+ {
+ private BeatmapManager beatmaps = null!;
+ private RulesetStore rulesets = null!;
+
+ private BeatmapSetInfo? importedSet;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, AudioManager audio)
+ {
+ Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
+ Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
+ Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, Scheduler, API));
+ Dependencies.Cache(Realm);
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("import beatmap", () =>
+ {
+ beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
+ importedSet = beatmaps.GetAllUsableBeatmapSets().First();
+ });
+ }
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => beatmaps.GetWorkingBeatmap(importedSet?.Beatmaps.First()).Beatmap;
+
+ private Ruleset? customRuleset;
+
+ protected override Ruleset CreatePlayerRuleset() => customRuleset ?? new OsuRuleset();
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
+
+ protected override bool HasCustomSteps => true;
+
+ protected override bool AllowFail => false;
+
+ [Test]
+ public void TestLastPlayedUpdated()
+ {
+ DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
+
+ AddStep("set no custom ruleset", () => customRuleset = null);
+ AddAssert("last played is null", () => getLastPlayed() == null);
+
+ CreateTest();
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+ AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
+ }
+
+ [Test]
+ public void TestScoreStoredLocally()
+ {
+ AddStep("set no custom ruleset", () => customRuleset = null);
+
+ CreateTest();
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null));
+ }
+
+ [Test]
+ public void TestScoreStoredLocallyCustomRuleset()
+ {
+ Ruleset createCustomRuleset() => new CustomRuleset();
+
+ AddStep("import custom ruleset", () => Realm.Write(r => r.Add(createCustomRuleset().RulesetInfo)));
+ AddStep("set custom ruleset", () => customRuleset = createCustomRuleset());
+
+ CreateTest();
+
+ AddAssert("score has custom ruleset", () => Player.Score.ScoreInfo.Ruleset.Equals(customRuleset.AsNonNull().RulesetInfo));
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null));
+ }
+
+ private class CustomRuleset : OsuRuleset, ILegacyRuleset
+ {
+ public override string Description => "custom";
+ public override string ShortName => "custom";
+
+ int ILegacyRuleset.LegacyID => -1;
+
+ public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
index e0c8989389..96efca6b65 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -365,21 +365,9 @@ namespace osu.Game.Tests.Visual.Gameplay
ImportedScore = score;
- // It was discovered that Score members could sometimes be half-populated.
- // In particular, the RulesetID property could be set to 0 even on non-osu! maps.
- // We want to test that the state of that property is consistent in this test.
- // EF makes this impossible.
- //
- // First off, because of the EF navigational property-explicit foreign key field duality,
- // it can happen that - for example - the Ruleset navigational property is correctly initialised to mania,
- // but the RulesetID foreign key property is not initialised and remains 0.
- // EF silently bypasses this by prioritising the Ruleset navigational property over the RulesetID foreign key one.
- //
- // Additionally, adding an entity to an EF DbSet CAUSES SIDE EFFECTS with regard to the foreign key property.
- // In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context,
- // RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3.
- //
- // For the above reasons, actual importing is disabled in this test.
+ // Calling base.ImportScore is omitted as it will fail for the test method which uses a custom ruleset.
+ // This can be resolved by doing something similar to what TestScenePlayerLocalScoreImport is doing,
+ // but requires a bit of restructuring.
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index 10a6b196b0..c259d5f0a8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -142,6 +142,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
+ [Test]
+ public void TestLocallyAvailableWithoutReplay()
+ {
+ Live imported = null;
+
+ AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(false, false)));
+
+ AddStep("create button without replay", () =>
+ {
+ Child = downloadButton = new TestReplayDownloadButton(imported.Value)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
+ });
+
+ AddUntilStep("wait for load", () => downloadButton.IsLoaded);
+
+ AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
+ AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
+ }
+
[Test]
public void TestScoreImportThenDelete()
{
@@ -189,11 +211,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value);
}
- private ScoreInfo getScoreInfo(bool replayAvailable)
+ private ScoreInfo getScoreInfo(bool replayAvailable, bool hasOnlineId = true)
{
return new APIScore
{
- OnlineID = online_score_id,
+ OnlineID = hasOnlineId ? online_score_id : 0,
RulesetID = 0,
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
HasReplay = replayAvailable,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
index 757dfff2b7..1797c82fb9 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Cursor;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -195,12 +196,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestDownloadButtonHiddenWhenBeatmapExists()
{
- var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
Live imported = null;
- Debug.Assert(beatmap.BeatmapSet != null);
+ AddStep("import beatmap", () =>
+ {
+ var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
- AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet));
+ Debug.Assert(beatmap.BeatmapSet != null);
+ imported = manager.Import(beatmap.BeatmapSet);
+ });
createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach()));
@@ -245,40 +249,35 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestExpiredItems()
{
- AddStep("create playlist", () =>
+ createPlaylist(p =>
{
- Child = playlist = new TestPlaylist
+ p.Items.Clear();
+ p.Items.AddRange(new[]
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300),
- Items =
+ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
- new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ ID = 0,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ Expired = true,
+ RequiredMods = new[]
{
- ID = 0,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- Expired = true,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
- },
- new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
+ }
+ },
+ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ {
+ ID = 1,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ RequiredMods = new[]
{
- ID = 1,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
}
}
- };
+ });
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
@@ -321,19 +320,44 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible);
+ private void createPlaylistWithBeatmaps(Func> beatmaps) => createPlaylist(p =>
+ {
+ int index = 0;
+
+ p.Items.Clear();
+
+ foreach (var b in beatmaps())
+ {
+ p.Items.Add(new PlaylistItem(b)
+ {
+ ID = index++,
+ OwnerID = 2,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ RequiredMods = new[]
+ {
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
+ }
+ });
+ }
+ });
+
private void createPlaylist(Action setupPlaylist = null)
{
AddStep("create playlist", () =>
{
- Child = playlist = new TestPlaylist
+ Child = new OsuContextMenuContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300)
+ RelativeSizeAxes = Axes.Both,
+ Child = playlist = new TestPlaylist
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500, 300)
+ }
};
- setupPlaylist?.Invoke(playlist);
-
for (int i = 0; i < 20; i++)
{
playlist.Items.Add(new PlaylistItem(i % 2 == 1
@@ -360,39 +384,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
- });
- AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
- }
-
- private void createPlaylistWithBeatmaps(Func> beatmaps)
- {
- AddStep("create playlist", () =>
- {
- Child = playlist = new TestPlaylist
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300)
- };
-
- int index = 0;
-
- foreach (var b in beatmaps())
- {
- playlist.Items.Add(new PlaylistItem(b)
- {
- ID = index++,
- OwnerID = 2,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
- });
- }
+ setupPlaylist?.Invoke(playlist);
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index a70dfd78c5..edd1491865 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
@@ -368,12 +369,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
ParticipantsList? participantsList = null;
- AddStep("create new list", () => Child = participantsList = new ParticipantsList
+ AddStep("create new list", () => Child = new OsuContextMenuContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Y,
- Size = new Vector2(380, 0.7f)
+ RelativeSizeAxes = Axes.Both,
+ Child = participantsList = new ParticipantsList
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ Size = new Vector2(380, 0.7f)
+ }
});
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
index 16a34e996f..be03328caa 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
@@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Users;
using osuTK.Graphics;
@@ -146,12 +147,12 @@ namespace osu.Game.Tests.Visual.Online
{
var scores = new APIScoresCollection
{
- Scores = new List
+ Scores = new List
{
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 6602580,
@@ -175,10 +176,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890,
Accuracy = 1,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 4608074,
@@ -201,10 +202,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789,
Accuracy = 0.9997,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 1014222,
@@ -226,10 +227,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678,
Accuracy = 0.9854,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 1541390,
@@ -250,10 +251,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567,
Accuracy = 0.8765,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 7151382,
@@ -275,12 +276,12 @@ namespace osu.Game.Tests.Visual.Online
foreach (var s in scores.Scores)
{
- s.Statistics = new Dictionary
+ s.Statistics = new Dictionary
{
- { "count_300", RNG.Next(2000) },
- { "count_100", RNG.Next(2000) },
- { "count_50", RNG.Next(2000) },
- { "count_miss", RNG.Next(2000) }
+ { HitResult.Great, RNG.Next(2000) },
+ { HitResult.Ok, RNG.Next(2000) },
+ { HitResult.Meh, RNG.Next(2000) },
+ { HitResult.Miss, RNG.Next(2000) }
};
}
@@ -289,10 +290,10 @@ namespace osu.Game.Tests.Visual.Online
private APIScoreWithPosition createUserBest() => new APIScoreWithPosition
{
- Score = new APIScore
+ Score = new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 7151382,
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index 1b9b59676b..ef0c7d7d4d 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default));
- dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler));
+ dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
return dependencies;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
index 8f890b2383..05b5c5c0cd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneTopLocalRank.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
- Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler));
+ Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index cee917f6cf..e59914f69a 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default));
- dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler));
+ dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get(), () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
return dependencies;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs
index 0a0415789a..ce9aa682d1 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs
@@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
@@ -13,10 +13,24 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModIcon : OsuTestScene
{
+ [Test]
+ public void TestShowAllMods()
+ {
+ AddStep("create mod icons", () =>
+ {
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Full,
+ ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)),
+ };
+ });
+ }
+
[Test]
public void TestChangeModType()
{
- ModIcon icon = null;
+ ModIcon icon = null!;
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
AddStep("change mod", () => icon.Mod = new OsuModEasy());
@@ -25,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestInterfaceModType()
{
- ModIcon icon = null;
+ ModIcon icon = null!;
var ruleset = new OsuRuleset();
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index a1eef4ce47..7615b3e8be 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -1,14 +1,13 @@
-
-
+
WinExe
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 6fd53d923b..5512b26863 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -4,7 +4,6 @@
osu.Game.Tournament.Tests.TournamentTestRunner
-
diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
index 0fc3646585..b088670caa 100644
--- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
+++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
@@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Components
{
public class TournamentSpriteTextWithBackground : CompositeDrawable
{
- protected readonly TournamentSpriteText Text;
+ public readonly TournamentSpriteText Text;
+
protected readonly Box Background;
public TournamentSpriteTextWithBackground(string text = "")
diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs
index c6bbb54f9a..2e79998e66 100644
--- a/osu.Game.Tournament/Components/TourneyVideo.cs
+++ b/osu.Game.Tournament/Components/TourneyVideo.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Components
private Video video;
private ManualClock manualClock;
+ public bool VideoAvailable => video != null;
+
public TourneyVideo(string filename, bool drawFallbackGradient = false)
{
this.filename = filename;
diff --git a/osu.Game.Tournament/Models/SeedingBeatmap.cs b/osu.Game.Tournament/Models/SeedingBeatmap.cs
index 03beb7ca9a..fb0e20556c 100644
--- a/osu.Game.Tournament/Models/SeedingBeatmap.cs
+++ b/osu.Game.Tournament/Models/SeedingBeatmap.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using Newtonsoft.Json;
using osu.Framework.Bindables;
@@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Models
public int ID;
[JsonProperty("BeatmapInfo")]
- public TournamentBeatmap Beatmap;
+ public TournamentBeatmap? Beatmap;
public long Score;
diff --git a/osu.Game.Tournament/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs
new file mode 100644
index 0000000000..b5e08fc005
--- /dev/null
+++ b/osu.Game.Tournament/SaveChangesOverlay.cs
@@ -0,0 +1,101 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.Tasks;
+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.Game.Graphics;
+using osu.Game.Online.Multiplayer;
+using osuTK;
+
+namespace osu.Game.Tournament
+{
+ internal class SaveChangesOverlay : CompositeDrawable
+ {
+ [Resolved]
+ private TournamentGame tournamentGame { get; set; } = null!;
+
+ private string? lastSerialisedLadder;
+ private readonly TourneyButton saveChangesButton;
+
+ public SaveChangesOverlay()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = new Container
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Position = new Vector2(5),
+ CornerRadius = 10,
+ Masking = true,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ saveChangesButton = new TourneyButton
+ {
+ Text = "Save Changes",
+ Width = 140,
+ Height = 50,
+ Padding = new MarginPadding
+ {
+ Top = 10,
+ Left = 10,
+ },
+ Margin = new MarginPadding
+ {
+ Right = 10,
+ Bottom = 10,
+ },
+ Action = saveChanges,
+ // Enabled = { Value = false },
+ },
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ scheduleNextCheck();
+ }
+
+ private async Task checkForChanges()
+ {
+ string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder());
+
+ // If a save hasn't been triggered by the user yet, populate the initial value
+ lastSerialisedLadder ??= serialisedLadder;
+
+ if (lastSerialisedLadder != serialisedLadder && !saveChangesButton.Enabled.Value)
+ {
+ saveChangesButton.Enabled.Value = true;
+ saveChangesButton.Background
+ .FadeColour(saveChangesButton.BackgroundColour.Lighten(0.5f), 500, Easing.In).Then()
+ .FadeColour(saveChangesButton.BackgroundColour, 500, Easing.Out)
+ .Loop();
+ }
+
+ scheduleNextCheck();
+ }
+
+ private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
+
+ private void saveChanges()
+ {
+ tournamentGame.SaveChanges();
+ lastSerialisedLadder = tournamentGame.GetSerialisedLadder();
+
+ saveChangesButton.Enabled.Value = false;
+ saveChangesButton.Background.FadeColour(saveChangesButton.BackgroundColour, 500);
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
index 32da4d1b36..5ac25f97b5 100644
--- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
+++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
@@ -12,8 +12,6 @@ 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.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics;
@@ -45,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Drawings
public ITeamList TeamList;
[BackgroundDependencyLoader]
- private void load(TextureStore textures, Storage storage)
+ private void load(Storage storage)
{
RelativeSizeAxes = Axes.Both;
@@ -91,11 +89,10 @@ namespace osu.Game.Tournament.Screens.Drawings
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- new Sprite
+ new TourneyVideo("drawings")
{
+ Loop = true,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fill,
- Texture = textures.Get(@"Backgrounds/Drawings/background.png")
},
// Visualiser
new VisualiserContainer
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index 11db37c8b7..111893d18c 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -298,10 +298,10 @@ namespace osu.Game.Tournament.Screens.Editors
}, true);
}
- private void updatePanel()
+ private void updatePanel() => Scheduler.AddOnce(() =>
{
drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 };
- }
+ });
}
}
}
diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
index 8af5bbe513..0fefe6f780 100644
--- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
@@ -20,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Editors
{
- public abstract class TournamentEditorScreen : TournamentScreen, IProvideVideo
+ public abstract class TournamentEditorScreen : TournamentScreen
where TDrawable : Drawable, IModelBacked
where TModel : class, new()
{
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
index bb187c9e67..1eceddd871 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
@@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{
private readonly TeamScore score;
+ private readonly TournamentSpriteTextWithBackground teamText;
+
+ private readonly Bindable teamName = new Bindable("???");
+
private bool showScore;
public bool ShowScore
@@ -93,7 +97,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
}
}
},
- new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???")
+ teamText = new TournamentSpriteTextWithBackground
{
Scale = new Vector2(0.5f),
Origin = anchor,
@@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
updateDisplay();
FinishTransforms(true);
+
+ if (Team != null)
+ teamName.BindTo(Team.FullName);
+
+ teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
}
private void updateDisplay()
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
index ed11f097ed..5ee57e9271 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
@@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
currentMatch.BindTo(ladder.CurrentMatch);
currentMatch.BindValueChanged(matchChanged);
+ currentTeam.BindValueChanged(teamChanged);
+
updateMatch();
}
@@ -67,7 +69,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
// team may change to same team, which means score is not in a good state.
// thus we handle this manually.
- teamChanged(currentTeam.Value);
+ currentTeam.TriggerChange();
}
protected override bool OnMouseDown(MouseDownEvent e)
@@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
return base.OnMouseDown(e);
}
- private void teamChanged(TournamentTeam team)
+ private void teamChanged(ValueChangedEvent team)
{
InternalChildren = new Drawable[]
{
- teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
+ teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
};
}
}
diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
index 86b2c2a4e9..54ae4c0366 100644
--- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
@@ -21,7 +21,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Gameplay
{
- public class GameplayScreen : BeatmapInfoScreen, IProvideVideo
+ public class GameplayScreen : BeatmapInfoScreen
{
private readonly BindableBool warmup = new BindableBool();
diff --git a/osu.Game.Tournament/Screens/IProvideVideo.cs b/osu.Game.Tournament/Screens/IProvideVideo.cs
deleted file mode 100644
index aa67a5211f..0000000000
--- a/osu.Game.Tournament/Screens/IProvideVideo.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-namespace osu.Game.Tournament.Screens
-{
- ///
- /// Marker interface for a screen which provides its own local video background.
- ///
- public interface IProvideVideo
- {
- }
-}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
index f0eda5672a..1fdf616e34 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -53,6 +53,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection =>
{
+ // ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
+ GetContainingInputManager().TriggerFocusContention(null);
+
roundDropdown.Current = selection.NewValue?.Round;
losersCheckbox.Current = selection.NewValue?.Losers;
dateTimeBox.Current = selection.NewValue?.Date;
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
index 23bfa84afc..7ad7e76a1f 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Ladder
{
- public class LadderScreen : TournamentScreen, IProvideVideo
+ public class LadderScreen : TournamentScreen
{
protected Container MatchesContainer;
private Container paths;
diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
index 7a11e26794..0827cbae69 100644
--- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
+++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Schedule
{
- public class ScheduleScreen : TournamentScreen // IProvidesVideo
+ public class ScheduleScreen : TournamentScreen
{
private readonly Bindable currentMatch = new Bindable();
private Container mainContainer;
diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
index 42eff3565f..2b2dce3664 100644
--- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
+++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
@@ -9,6 +9,8 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Overlays;
@@ -19,7 +21,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Setup
{
- public class SetupScreen : TournamentScreen, IProvideVideo
+ public class SetupScreen : TournamentScreen
{
private FillFlowContainer fillFlow;
@@ -48,13 +50,21 @@ namespace osu.Game.Tournament.Screens.Setup
{
windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize);
- InternalChild = fillFlow = new FillFlowContainer
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Padding = new MarginPadding(10),
- Spacing = new Vector2(10),
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.2f),
+ },
+ fillFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(10),
+ Spacing = new Vector2(10),
+ }
};
api.LocalUser.BindValueChanged(_ => Schedule(reload));
@@ -74,7 +84,8 @@ namespace osu.Game.Tournament.Screens.Setup
Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()),
Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found",
Failing = fileBasedIpc?.IPCStorage == null,
- Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
+ Description =
+ "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
},
new ActionableInfo
{
diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
index 082aa99b0e..a7a175ceba 100644
--- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
+++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
@@ -14,7 +14,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Showcase
{
- public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo
+ public class ShowcaseScreen : BeatmapInfoScreen
{
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
index 925c697346..9262cab098 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -19,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro
{
- public class SeedingScreen : TournamentMatchScreen, IProvideVideo
+ public class SeedingScreen : TournamentMatchScreen
{
private Container mainContainer;
@@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
currentTeam.BindValueChanged(teamChanged, true);
}
- private void teamChanged(ValueChangedEvent team)
+ private void teamChanged(ValueChangedEvent team) => Scheduler.AddOnce(() =>
{
if (team.NewValue == null)
{
@@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
}
showTeam(team.NewValue);
- }
+ });
protected override void CurrentMatchChanged(ValueChangedEvent match)
{
@@ -120,8 +121,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro
foreach (var seeding in team.SeedingResults)
{
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
+
foreach (var beatmap in seeding.Beatmaps)
+ {
+ if (beatmap.Beatmap == null)
+ continue;
+
fill.Add(new BeatmapScoreRow(beatmap));
+ }
}
}
@@ -129,6 +136,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
{
public BeatmapScoreRow(SeedingBeatmap beatmap)
{
+ Debug.Assert(beatmap.Beatmap != null);
+
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Children = new Drawable[]
{
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
- new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
+ new TournamentSpriteText
+ { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
}
},
};
diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
index 98dfaa7487..08c9a7a897 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
@@ -13,7 +13,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro
{
- public class TeamIntroScreen : TournamentMatchScreen, IProvideVideo
+ public class TeamIntroScreen : TournamentMatchScreen
{
private Container mainContainer;
diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
index 50207547cd..ac54ff58f5 100644
--- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
@@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamWin
{
- public class TeamWinScreen : TournamentMatchScreen, IProvideVideo
+ public class TeamWinScreen : TournamentMatchScreen
{
private Container mainContainer;
@@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
private bool firstDisplay = true;
- private void update() => Schedule(() =>
+ private void update() => Scheduler.AddOnce(() =>
{
var match = CurrentMatch.Value;
diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index 537fbfc038..7d67bfa759 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -11,8 +11,6 @@ using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Logging;
using osu.Framework.Platform;
@@ -20,11 +18,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.Models;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tournament
{
+ [Cached]
public class TournamentGame : TournamentGameBase
{
public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE;
@@ -78,40 +76,9 @@ namespace osu.Game.Tournament
LoadComponentsAsync(new[]
{
- new Container
+ new SaveChangesOverlay
{
- CornerRadius = 10,
Depth = float.MinValue,
- Position = new Vector2(5),
- Masking = true,
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Children = new Drawable[]
- {
- new Box
- {
- Colour = OsuColour.Gray(0.2f),
- RelativeSizeAxes = Axes.Both,
- },
- new TourneyButton
- {
- Text = "Save Changes",
- Width = 140,
- Height = 50,
- Padding = new MarginPadding
- {
- Top = 10,
- Left = 10,
- },
- Margin = new MarginPadding
- {
- Right = 10,
- Bottom = 10,
- },
- Action = SaveChanges,
- },
- }
},
heightWarning = new WarningBox("Please make the window wider")
{
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 75c9f17d4c..f2a35ea5b3 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -295,7 +295,7 @@ namespace osu.Game.Tournament
}
}
- protected virtual void SaveChanges()
+ public void SaveChanges()
{
if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)
{
@@ -311,7 +311,16 @@ namespace osu.Game.Tournament
.ToList();
// Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state.
- string serialisedLadder = JsonConvert.SerializeObject(ladder,
+ string serialisedLadder = GetSerialisedLadder();
+
+ using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
+ using (var sw = new StreamWriter(stream))
+ sw.Write(serialisedLadder);
+ }
+
+ public string GetSerialisedLadder()
+ {
+ return JsonConvert.SerializeObject(ladder,
new JsonSerializerSettings
{
Formatting = Formatting.Indented,
@@ -319,10 +328,6 @@ namespace osu.Game.Tournament
DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() }
});
-
- using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
- using (var sw = new StreamWriter(stream))
- sw.Write(serialisedLadder);
}
protected override UserInputManager CreateUserInputManager() => new TournamentInputManager();
diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs
index 296b259d72..a12dbb4740 100644
--- a/osu.Game.Tournament/TournamentSceneManager.cs
+++ b/osu.Game.Tournament/TournamentSceneManager.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -186,7 +187,7 @@ namespace osu.Game.Tournament
var lastScreen = currentScreen;
currentScreen = target;
- if (currentScreen is IProvideVideo)
+ if (currentScreen.ChildrenOfType().FirstOrDefault()?.VideoAvailable == true)
{
video.FadeOut(200);
diff --git a/osu.Game.Tournament/TourneyButton.cs b/osu.Game.Tournament/TourneyButton.cs
index f5a82771f5..f1b14df783 100644
--- a/osu.Game.Tournament/TourneyButton.cs
+++ b/osu.Game.Tournament/TourneyButton.cs
@@ -3,12 +3,15 @@
#nullable disable
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tournament
{
public class TourneyButton : OsuButton
{
+ public new Box Background => base.Background;
+
public TourneyButton()
: base(null)
{
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index 92f1fc17d5..3e4d01a9a3 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -80,9 +80,8 @@ namespace osu.Game.Beatmaps
if (beatmapSet.OnlineID > 0)
{
- var existingSetWithSameOnlineID = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
-
- if (existingSetWithSameOnlineID != null)
+ // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure.
+ foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == beatmapSet.OnlineID))
{
existingSetWithSameOnlineID.DeletePending = true;
existingSetWithSameOnlineID.OnlineID = -1;
@@ -90,7 +89,7 @@ namespace osu.Game.Beatmaps
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineID = -1;
- LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted.");
+ LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion.");
}
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 346bf86818..45d76259fc 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
@@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
public bool SamplesMatchPlaybackRate { get; set; } = true;
+ ///
+ /// The time at which this beatmap was last played by the local user.
+ ///
+ public DateTimeOffset? LastPlayed { get; set; }
+
///
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see ).
@@ -151,14 +157,23 @@ namespace osu.Game.Beatmaps
public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
- && BeatmapSet.Hash == other.BeatmapSet.Hash
- && Metadata.AudioFile == other.Metadata.AudioFile;
+ && compareFiles(this, other, m => m.AudioFile);
public bool BackgroundEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
- && BeatmapSet.Hash == other.BeatmapSet.Hash
- && Metadata.BackgroundFile == other.Metadata.BackgroundFile;
+ && compareFiles(this, other, m => m.BackgroundFile);
+
+ private static bool compareFiles(BeatmapInfo x, BeatmapInfo y, Func getFilename)
+ {
+ Debug.Assert(x.BeatmapSet != null);
+ Debug.Assert(y.BeatmapSet != null);
+
+ string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.BeatmapSet.Metadata))?.File.Hash;
+ string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.BeatmapSet.Metadata))?.File.Hash;
+
+ return fileHashX == fileHashY;
+ }
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
diff --git a/osu.Game/Collections/CollectionToggleMenuItem.cs b/osu.Game/Collections/CollectionToggleMenuItem.cs
new file mode 100644
index 0000000000..f2b10305b8
--- /dev/null
+++ b/osu.Game/Collections/CollectionToggleMenuItem.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.Beatmaps;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Collections
+{
+ public class CollectionToggleMenuItem : ToggleMenuItem
+ {
+ public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap)
+ : base(collection.Name.Value, MenuItemType.Standard, state =>
+ {
+ if (state)
+ collection.BeatmapHashes.Add(beatmap.MD5Hash);
+ else
+ collection.BeatmapHashes.Remove(beatmap.MD5Hash);
+ })
+ {
+ State.Value = collection.BeatmapHashes.Contains(beatmap.MD5Hash);
+ }
+ }
+}
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 8cf57b802b..02b5a51f1f 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -58,8 +58,9 @@ namespace osu.Game.Database
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
+ /// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
///
- private const int schema_version = 14;
+ private const int schema_version = 15;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs
index 6cea92f1d9..aa7fac07a8 100644
--- a/osu.Game/Database/RealmArchiveModelImporter.cs
+++ b/osu.Game/Database/RealmArchiveModelImporter.cs
@@ -338,11 +338,11 @@ namespace osu.Game.Database
// import to store
realm.Add(item);
+ PostImport(item, realm);
+
transaction.Commit();
}
- PostImport(item, realm);
-
LogForModel(item, @"Import successfully completed!");
}
catch (Exception e)
@@ -479,7 +479,7 @@ namespace osu.Game.Database
}
///
- /// Perform any final actions after the import has been committed to the database.
+ /// Perform any final actions before the import has been committed to the database.
///
/// The model prepared for import.
/// The current realm context.
diff --git a/osu.Game/IO/LineBufferedReader.cs b/osu.Game/IO/LineBufferedReader.cs
index 93e554b43d..da1cdba73b 100644
--- a/osu.Game/IO/LineBufferedReader.cs
+++ b/osu.Game/IO/LineBufferedReader.cs
@@ -19,7 +19,7 @@ namespace osu.Game.IO
public LineBufferedReader(Stream stream, bool leaveOpen = false)
{
- streamReader = new StreamReader(stream, Encoding.UTF8, true, -1, leaveOpen);
+ streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen);
}
///
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 89bdd09f0d..368ac56850 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -94,6 +94,8 @@ namespace osu.Game.IO
error = OsuStorageError.None;
Storage lastStorage = UnderlyingStorage;
+ Logger.Log($"Attempting to use custom storage location {CustomStoragePath}");
+
try
{
Storage userStorage = host.GetStorage(CustomStoragePath);
@@ -102,6 +104,7 @@ namespace osu.Game.IO
error = OsuStorageError.AccessibleButEmpty;
ChangeTargetStorage(userStorage);
+ Logger.Log($"Storage successfully changed to {CustomStoragePath}.");
}
catch
{
@@ -109,6 +112,9 @@ namespace osu.Game.IO
ChangeTargetStorage(lastStorage);
}
+ if (error != OsuStorageError.None)
+ Logger.Log($"Custom storage location could not be used ({error}).");
+
return error == OsuStorageError.None;
}
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 088dc56701..43cea7fb97 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Online.API
public string WebsiteRootUrl { get; }
- public int APIVersion => 20220217; // We may want to pull this from the game version eventually.
+ public int APIVersion => 20220705; // We may want to pull this from the game version eventually.
public Exception LastLoginError { get; private set; }
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
index 8bd54f889d..494826f534 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
@@ -16,11 +16,11 @@ namespace osu.Game.Online.API.Requests.Responses
public int? Position;
[JsonProperty(@"score")]
- public APIScore Score;
+ public SoloScoreInfo Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
- var score = Score.CreateScoreInfo(rulesets, beatmap);
+ var score = Score.ToScoreInfo(rulesets, beatmap);
score.Position = Position;
return score;
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
index 9c8a38c63a..38c67d92f4 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIScoresCollection
{
[JsonProperty(@"scores")]
- public List Scores;
+ public List Scores;
[JsonProperty(@"userScore")]
public APIScoreWithPosition UserScore;
diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
new file mode 100644
index 0000000000..b70da194a5
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
@@ -0,0 +1,143 @@
+// 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 Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ [Serializable]
+ public class SoloScoreInfo : IHasOnlineID
+ {
+ [JsonProperty("replay")]
+ public bool HasReplay { get; set; }
+
+ [JsonProperty("beatmap_id")]
+ public int BeatmapID { get; set; }
+
+ [JsonProperty("ruleset_id")]
+ public int RulesetID { get; set; }
+
+ [JsonProperty("build_id")]
+ public int? BuildID { get; set; }
+
+ [JsonProperty("passed")]
+ public bool Passed { get; set; }
+
+ [JsonProperty("total_score")]
+ public int TotalScore { get; set; }
+
+ [JsonProperty("accuracy")]
+ public double Accuracy { get; set; }
+
+ [JsonProperty("user_id")]
+ public int UserID { get; set; }
+
+ // TODO: probably want to update this column to match user stats (short)?
+ [JsonProperty("max_combo")]
+ public int MaxCombo { get; set; }
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ [JsonProperty("rank")]
+ public ScoreRank Rank { get; set; }
+
+ [JsonProperty("started_at")]
+ public DateTimeOffset? StartedAt { get; set; }
+
+ [JsonProperty("ended_at")]
+ public DateTimeOffset? EndedAt { get; set; }
+
+ [JsonProperty("mods")]
+ public APIMod[] Mods { get; set; } = Array.Empty();
+
+ [JsonIgnore]
+ [JsonProperty("created_at")]
+ public DateTimeOffset CreatedAt { get; set; }
+
+ [JsonIgnore]
+ [JsonProperty("updated_at")]
+ public DateTimeOffset UpdatedAt { get; set; }
+
+ [JsonIgnore]
+ [JsonProperty("deleted_at")]
+ public DateTimeOffset? DeletedAt { get; set; }
+
+ [JsonProperty("statistics")]
+ public Dictionary Statistics { get; set; } = new Dictionary();
+
+ #region osu-web API additions (not stored to database).
+
+ [JsonProperty("id")]
+ public long? ID { get; set; }
+
+ [JsonProperty("user")]
+ public APIUser? User { get; set; }
+
+ [JsonProperty("pp")]
+ public double? PP { get; set; }
+
+ #endregion
+
+ public override string ToString() => $"score_id: {ID} user_id: {UserID}";
+
+ ///
+ /// Create a from an API score instance.
+ ///
+ /// A ruleset store, used to populate a ruleset instance in the returned score.
+ /// An optional beatmap, copied into the returned score (for cases where the API does not populate the beatmap).
+ ///
+ public ScoreInfo ToScoreInfo(RulesetStore rulesets, BeatmapInfo? beatmap = null)
+ {
+ var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally");
+
+ var rulesetInstance = ruleset.CreateInstance();
+
+ var mods = Mods.Select(apiMod => rulesetInstance.CreateModFromAcronym(apiMod.Acronym)).Where(m => m != null).ToArray();
+
+ // all API scores provided by this class are considered to be legacy.
+ mods = mods.Append(rulesetInstance.CreateMod()).ToArray();
+
+ var scoreInfo = ToScoreInfo(mods);
+
+ scoreInfo.Ruleset = ruleset;
+ if (beatmap != null) scoreInfo.BeatmapInfo = beatmap;
+
+ return scoreInfo;
+ }
+
+ ///
+ /// Create a from an API score instance.
+ ///
+ /// The mod instances, resolved from a ruleset.
+ ///
+ public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo
+ {
+ OnlineID = OnlineID,
+ User = User ?? new APIUser { Id = UserID },
+ BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
+ Ruleset = new RulesetInfo { OnlineID = RulesetID },
+ Passed = Passed,
+ TotalScore = TotalScore,
+ Accuracy = Accuracy,
+ MaxCombo = MaxCombo,
+ Rank = Rank,
+ Statistics = Statistics,
+ Date = EndedAt ?? DateTimeOffset.Now,
+ Hash = "online", // TODO: temporary?
+ HasReplay = HasReplay,
+ Mods = mods,
+ PP = PP,
+ };
+
+ public long OnlineID => ID ?? -1;
+ }
+}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index a16252ac89..4b5c9c0815 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -272,7 +272,7 @@ namespace osu.Game
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
- dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig));
+ dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, API, difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index c2e54d0d7b..e50fc356eb 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
MD5Hash = apiBeatmap.MD5Hash
};
- scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
+ scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
.ContinueWith(task => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
@@ -101,7 +101,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
scoreTable.Show();
var userScore = value.UserScore;
- var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets, beatmapInfo);
+ var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo);
topScoresContainer.Add(new DrawableTopScore(topScore));
diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs
index a07cf1608d..493cd66258 100644
--- a/osu.Game/Overlays/DialogOverlay.cs
+++ b/osu.Game/Overlays/DialogOverlay.cs
@@ -49,43 +49,54 @@ namespace osu.Game.Overlays
public void Push(PopupDialog dialog)
{
- if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
-
- var lastDialog = CurrentDialog;
+ if (dialog == CurrentDialog || dialog.State.Value == Visibility.Hidden) return;
// Immediately update the externally accessible property as this may be used for checks even before
// a DialogOverlay instance has finished loading.
+ var lastDialog = CurrentDialog;
CurrentDialog = dialog;
- Scheduler.Add(() =>
+ Schedule(() =>
{
// if any existing dialog is being displayed, dismiss it before showing a new one.
lastDialog?.Hide();
- dialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
- dialogContainer.Add(dialog);
+ // if the new dialog is hidden before added to the dialogContainer, bypass any further operations.
+ if (dialog.State.Value == Visibility.Hidden)
+ {
+ dismiss();
+ return;
+ }
+
+ dialogContainer.Add(dialog);
Show();
- }, false);
+
+ dialog.State.BindValueChanged(state =>
+ {
+ if (state.NewValue != Visibility.Hidden) return;
+
+ // Trigger the demise of the dialog as soon as it hides.
+ dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
+
+ dismiss();
+ });
+ });
+
+ void dismiss()
+ {
+ if (dialog != CurrentDialog) return;
+
+ // Handle the case where the dialog is the currently displayed dialog.
+ // In this scenario, the overlay itself should also be hidden.
+ Hide();
+ CurrentDialog = null;
+ }
}
public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0;
protected override bool BlockNonPositionalInput => true;
- private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v)
- {
- if (v != Visibility.Hidden) return;
-
- // handle the dialog being dismissed.
- dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
-
- if (dialog == CurrentDialog)
- {
- Hide();
- CurrentDialog = null;
- }
- }
-
protected override void PopIn()
{
base.PopIn();
@@ -97,7 +108,8 @@ namespace osu.Game.Overlays
base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
- if (CurrentDialog?.State.Value == Visibility.Visible)
+ // PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present.
+ if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible)
CurrentDialog.Hide();
}
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
index 20ff8f21c8..cb1e96d2f2 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
@@ -1,14 +1,24 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Configuration;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Framework.Threading;
+using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
+using osuTK;
namespace osu.Game.Overlays.FirstRunSetup
{
@@ -20,13 +30,175 @@ namespace osu.Game.Overlays.FirstRunSetup
{
Content.Children = new Drawable[]
{
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ RowDimensions = new[]
+ {
+ // Avoid height changes when changing language.
+ new Dimension(GridSizeMode.AutoSize, minSize: 100),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
+ {
+ Text = FirstRunSetupOverlayStrings.WelcomeDescription,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ },
+ },
+ }
+ },
+ new LanguageSelectionFlow
{
- Text = FirstRunSetupOverlayStrings.WelcomeDescription,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
- },
+ }
};
}
+
+ private class LanguageSelectionFlow : FillFlowContainer
+ {
+ private Bindable frameworkLocale = null!;
+
+ private ScheduledDelegate? updateSelectedDelegate;
+
+ [BackgroundDependencyLoader]
+ private void load(FrameworkConfigManager frameworkConfig)
+ {
+ Direction = FillDirection.Full;
+ Spacing = new Vector2(5);
+
+ ChildrenEnumerable = Enum.GetValues(typeof(Language))
+ .Cast()
+ .Select(l => new LanguageButton(l)
+ {
+ Action = () => frameworkLocale.Value = l.ToCultureCode()
+ });
+
+ frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale);
+ frameworkLocale.BindValueChanged(locale =>
+ {
+ if (!LanguageExtensions.TryParseCultureCode(locale.NewValue, out var language))
+ language = Language.en;
+
+ // Changing language may cause a short period of blocking the UI thread while the new glyphs are loaded.
+ // Scheduling ensures the button animation plays smoothly after any blocking operation completes.
+ // Note that a delay is required (the alternative would be a double-schedule; delay feels better).
+ updateSelectedDelegate?.Cancel();
+ updateSelectedDelegate = Scheduler.AddDelayed(() => updateSelectedStates(language), 50);
+ }, true);
+ }
+
+ private void updateSelectedStates(Language language)
+ {
+ foreach (var c in Children.OfType())
+ c.Selected = c.Language == language;
+ }
+
+ private class LanguageButton : OsuClickableContainer
+ {
+ public readonly Language Language;
+
+ private Box backgroundBox = null!;
+
+ private OsuSpriteText text = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ private bool selected;
+
+ public bool Selected
+ {
+ get => selected;
+ set
+ {
+ if (selected == value)
+ return;
+
+ selected = value;
+
+ if (IsLoaded)
+ updateState();
+ }
+ }
+
+ public LanguageButton(Language language)
+ {
+ Language = language;
+
+ Size = new Vector2(160, 50);
+ Masking = true;
+ CornerRadius = 10;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ backgroundBox = new Box
+ {
+ Alpha = 0,
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
+ },
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = colourProvider.Light1,
+ Text = Language.GetDescription(),
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateState();
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ if (!selected)
+ updateState();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ if (!selected)
+ updateState();
+ base.OnHoverLost(e);
+ }
+
+ private void updateState()
+ {
+ if (selected)
+ {
+ const double selected_duration = 1000;
+
+ backgroundBox.FadeTo(1, selected_duration, Easing.OutQuint);
+ backgroundBox.FadeColour(colourProvider.Background2, selected_duration, Easing.OutQuint);
+ text.FadeColour(colourProvider.Content1, selected_duration, Easing.OutQuint);
+ text.ScaleTo(1.2f, selected_duration, Easing.OutQuint);
+ }
+ else
+ {
+ const double duration = 500;
+
+ backgroundBox.FadeTo(IsHovered ? 1 : 0, duration, Easing.OutQuint);
+ backgroundBox.FadeColour(colourProvider.Background5, duration, Easing.OutQuint);
+ text.FadeColour(colourProvider.Light1, duration, Easing.OutQuint);
+ text.ScaleTo(1, duration, Easing.OutQuint);
+ }
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs
index 0f21a497b0..b1f355a789 100644
--- a/osu.Game/Rulesets/UI/ModIcon.cs
+++ b/osu.Game/Rulesets/UI/ModIcon.cs
@@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osuTK;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.UI
@@ -53,7 +54,6 @@ namespace osu.Game.Rulesets.UI
private OsuColour colours { get; set; }
private Color4 backgroundColour;
- private Color4 highlightedColour;
///
/// Construct a new instance.
@@ -123,47 +123,13 @@ namespace osu.Game.Rulesets.UI
modAcronym.FadeOut();
}
- switch (value.Type)
- {
- default:
- case ModType.DifficultyIncrease:
- backgroundColour = colours.Yellow;
- highlightedColour = colours.YellowLight;
- break;
-
- case ModType.DifficultyReduction:
- backgroundColour = colours.Green;
- highlightedColour = colours.GreenLight;
- break;
-
- case ModType.Automation:
- backgroundColour = colours.Blue;
- highlightedColour = colours.BlueLight;
- break;
-
- case ModType.Conversion:
- backgroundColour = colours.Purple;
- highlightedColour = colours.PurpleLight;
- break;
-
- case ModType.Fun:
- backgroundColour = colours.Pink;
- highlightedColour = colours.PinkLight;
- break;
-
- case ModType.System:
- backgroundColour = colours.Gray6;
- highlightedColour = colours.Gray7;
- modIcon.Colour = colours.Yellow;
- break;
- }
-
+ backgroundColour = colours.ForModType(value.Type);
updateColour();
}
private void updateColour()
{
- background.Colour = Selected.Value ? highlightedColour : backgroundColour;
+ background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour;
}
}
}
diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs
index f59ffc7c94..53dd511d57 100644
--- a/osu.Game/Scoring/ScoreImporter.cs
+++ b/osu.Game/Scoring/ScoreImporter.cs
@@ -13,6 +13,9 @@ using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Rulesets;
using osu.Game.Scoring.Legacy;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
using Realms;
namespace osu.Game.Scoring
@@ -26,11 +29,14 @@ namespace osu.Game.Scoring
private readonly RulesetStore rulesets;
private readonly Func beatmaps;
- public ScoreImporter(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm)
+ private readonly IAPIProvider api;
+
+ public ScoreImporter(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, IAPIProvider api)
: base(storage, realm)
{
this.rulesets = rulesets;
this.beatmaps = beatmaps;
+ this.api = api;
}
protected override ScoreInfo? CreateModel(ArchiveReader archive)
@@ -68,5 +74,17 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(model.StatisticsJson))
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
}
+
+ protected override void PostImport(ScoreInfo model, Realm realm)
+ {
+ base.PostImport(model, realm);
+
+ var userRequest = new GetUserRequest(model.RealmUser.Username);
+
+ api.Perform(userRequest);
+
+ if (userRequest.Response is APIUser user)
+ model.RealmUser.OnlineID = user.Id;
+ }
}
}
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 6ee1d11f83..9aed8904e6 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -21,6 +21,7 @@ using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Online.API;
namespace osu.Game.Scoring
{
@@ -31,7 +32,7 @@ namespace osu.Game.Scoring
private readonly OsuConfigManager configManager;
private readonly ScoreImporter scoreImporter;
- public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler,
+ public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, IAPIProvider api,
BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null)
: base(storage, realm)
{
@@ -39,7 +40,7 @@ namespace osu.Game.Scoring
this.difficultyCache = difficultyCache;
this.configManager = configManager;
- scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm)
+ scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api)
{
PostNotification = obj => PostNotification?.Invoke(obj)
};
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 6ec9ff4e89..48576b81e2 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
// required so we can get the track length in EditorClock.
- // this is safe as nothing has yet got a reference to this new beatmap.
+ // this is ONLY safe because the track being provided is a `TrackVirtual` which we don't really care about disposing.
loadableBeatmap.LoadTrack();
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
@@ -921,7 +921,7 @@ namespace osu.Game.Screens.Edit
private void cancelExit()
{
- samplePlaybackDisabled.Value = false;
+ updateSampleDisabledState();
loader?.CancelPendingDifficultySwitch();
}
diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
index 87e640badc..fd230a97bc 100644
--- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
+++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs
@@ -35,7 +35,13 @@ namespace osu.Game.Screens.Edit.GameplayTest
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
{
if (completed.NewValue)
- Scheduler.AddDelayed(this.Exit, RESULTS_DISPLAY_DELAY);
+ {
+ Scheduler.AddDelayed(() =>
+ {
+ if (this.IsCurrentScreen())
+ this.Exit();
+ }, RESULTS_DISPLAY_DELAY);
+ }
});
}
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index 3cdf51a87c..6ad0350e43 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -72,9 +72,17 @@ namespace osu.Game.Screens.Menu
RelativeSizeAxes = Axes.Both,
Clock = decoupledClock,
LoadMenu = LoadMenu
- }, t =>
+ }, _ =>
{
- AddInternal(t);
+ AddInternal(intro);
+
+ // There is a chance that the intro timed out before being displayed, and this scheduled callback could
+ // happen during the outro rather than intro.
+ // In such a scenario, we don't want to play the intro sample, nor attempt to start the intro track
+ // (that may have already been since disposed by MusicController).
+ if (DidLoadMenu)
+ return;
+
if (!UsingThemedIntro)
welcome?.Play();
diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
index a3a176477e..f38077a9a7 100644
--- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
+++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs
@@ -9,17 +9,20 @@ using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
+using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -27,6 +30,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
@@ -38,7 +42,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay
{
- public class DrawableRoomPlaylistItem : OsuRearrangeableListItem
+ public class DrawableRoomPlaylistItem : OsuRearrangeableListItem, IHasContextMenu
{
public const float HEIGHT = 50;
@@ -93,6 +97,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private RulesetStore rulesets { get; set; }
+ [Resolved]
+ private BeatmapManager beatmaps { get; set; }
+
[Resolved]
private OsuColour colours { get; set; }
@@ -102,6 +109,15 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; }
+ [Resolved(CanBeNull = true)]
+ private BeatmapSetOverlay beatmapOverlay { get; set; }
+
+ [Resolved(CanBeNull = true)]
+ private CollectionManager collectionManager { get; set; }
+
+ [Resolved(CanBeNull = true)]
+ private ManageCollectionsDialog manageCollectionsDialog { get; set; }
+
protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model;
public DrawableRoomPlaylistItem(PlaylistItem item)
@@ -433,7 +449,7 @@ namespace osu.Game.Screens.OnlinePlay
}
}
},
- }
+ },
};
}
@@ -470,6 +486,31 @@ namespace osu.Game.Screens.OnlinePlay
return true;
}
+ public MenuItem[] ContextMenuItems
+ {
+ get
+ {
+ List