diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index cbd0231fdb..57694d7f57 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
- "version": "2022.607.0",
+ "version": "2022.809.0",
"commands": [
"localisation"
]
diff --git a/.globalconfig b/.globalconfig
index 462dbc74ed..a7b652c454 100644
--- a/.globalconfig
+++ b/.globalconfig
@@ -53,3 +53,7 @@ dotnet_diagnostic.CA2225.severity = none
# Banned APIs
dotnet_diagnostic.RS0030.severity = error
+
+# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
+# See: https://github.com/ppy/osu/pull/19677
+dotnet_diagnostic.OSUF001.severity = none
\ No newline at end of file
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
index 8f0b31ef1b..0a4fa84ce1 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
@@ -21,8 +21,11 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
public PippidonBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
: base(beatmap, ruleset)
{
- minPosition = beatmap.HitObjects.Min(getUsablePosition);
- maxPosition = beatmap.HitObjects.Max(getUsablePosition);
+ if (beatmap.HitObjects.Any())
+ {
+ minPosition = beatmap.HitObjects.Min(getUsablePosition);
+ maxPosition = beatmap.HitObjects.Max(getUsablePosition);
+ }
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition && h is IHasYPosition);
diff --git a/osu.Android.props b/osu.Android.props
index 6dbc6cc377..17a6178641 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index f832d99807..ed151855b1 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -23,6 +23,7 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
using osu.Game.Rulesets.Edit;
@@ -162,7 +163,7 @@ namespace osu.Game.Rulesets.Catch
};
}
- public override string GetDisplayNameForHitResult(HitResult result)
+ public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
index 16ef56d845..cac5b9aa6a 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs
@@ -1,12 +1,13 @@
// 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.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModEasy : ModEasyWithExtraLives
{
- public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
+ public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
index 63203dd57c..e12181d051 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFloatingFruits.cs
@@ -3,6 +3,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override string Name => "Floating Fruits";
public override string Acronym => "FF";
- public override string Description => "The fruits are... floating?";
+ public override LocalisableString Description => "The fruits are... floating?";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
index 51516edacd..d68430b64f 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
@@ -3,6 +3,7 @@
using System.Linq;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset
{
- public override string Description => @"Play with fading fruits.";
+ public override LocalisableString Description => @"Play with fading fruits.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
private const double fade_out_offset_multiplier = 0.6;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs
index a97e940a64..4cd2efdc2f 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModMirror.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModMirror : ModMirror, IApplicableToBeatmap
{
- public override string Description => "Fruits are flipped horizontally.";
+ public override LocalisableString Description => "Fruits are flipped horizontally.";
///
/// is used instead of ,
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
index a24a6227fe..9038153e20 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Framework.Utils;
using osu.Game.Configuration;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield
{
- public override string Description => "Where's the catcher?";
+ public override LocalisableString Description => "Where's the catcher?";
[SettingSource(
"Hidden at combo",
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 60f1614d98..69ae8328e9 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModRelax : ModRelax, IApplicableToDrawableRuleset, IApplicableToPlayer
{
- public override string Description => @"Use the mouse to control the catcher.";
+ public override LocalisableString Description => @"Use the mouse to control the catcher.";
private DrawableRuleset drawableRuleset = null!;
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index 4723416c30..ac6060ceed 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -15,6 +15,7 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -311,7 +312,7 @@ namespace osu.Game.Rulesets.Mania
return Array.Empty();
}
- public override string GetVariantName(int variant)
+ public override LocalisableString GetVariantName(int variant)
{
switch (getPlayfieldType(variant))
{
@@ -356,7 +357,7 @@ namespace osu.Game.Rulesets.Mania
};
}
- public override string GetDisplayNameForHitResult(HitResult result)
+ public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
index 73dfaaa878..66269f5572 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 0.9;
- public override string Description => "No more tricky speed changes!";
+ public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
index c78bf72979..2457aa75d7 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Dual Stages";
public override string Acronym => "DS";
- public override string Description => @"Double the stages, double the fun!";
+ public override LocalisableString Description => @"Double the stages, double the fun!";
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs
index 4093aeb2a7..5c8cd6a5ae 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs
@@ -1,12 +1,13 @@
// 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.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModEasy : ModEasyWithExtraLives
{
- public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!";
+ public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index f80c9e1f7c..c6e9c339f4 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
@@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Name => "Fade In";
public override string Acronym => "FI";
- public override string Description => @"Keys appear out of nowhere!";
+ public override LocalisableString Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray();
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
index e3ac624a6e..eeb6e94fc7 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
@@ -3,13 +3,14 @@
using System;
using System.Linq;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHidden : ManiaModPlayfieldCover
{
- public override string Description => @"Keys fade out before you hit them!";
+ public override LocalisableString Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray();
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
index a65938184c..ca9bc89473 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
@@ -8,6 +8,7 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.Beatmaps;
namespace osu.Game.Rulesets.Mania.Mods
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1;
- public override string Description => @"Replaces all hold notes with normal notes.";
+ public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
public override IconUsage? Icon => FontAwesome.Solid.DotCircle;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
index 4cbdaee323..ef9154d180 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => "IN";
public override double ScoreMultiplier => 1;
- public override string Description => "Hold the keys. To the beat.";
+ public override LocalisableString Description => "Hold the keys. To the beat.";
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
index 948979505c..31f52610e9 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey1 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 1;
public override string Name => "One Key";
public override string Acronym => "1K";
- public override string Description => @"Play with one key.";
+ public override LocalisableString Description => @"Play with one key.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
index 684370fc3d..67e65b887a 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey10.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey10 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 10;
public override string Name => "Ten Keys";
public override string Acronym => "10K";
- public override string Description => @"Play with ten keys.";
+ public override LocalisableString Description => @"Play with ten keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
index de91902ca8..0f8148d252 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey2 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 2;
public override string Name => "Two Keys";
public override string Acronym => "2K";
- public override string Description => @"Play with two keys.";
+ public override LocalisableString Description => @"Play with two keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
index 8575a96bde..0f8af7940c 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey3 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 3;
public override string Name => "Three Keys";
public override string Acronym => "3K";
- public override string Description => @"Play with three keys.";
+ public override LocalisableString Description => @"Play with three keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs
index 54ea3afa07..d3a4546dce 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey4 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 4;
public override string Name => "Four Keys";
public override string Acronym => "4K";
- public override string Description => @"Play with four keys.";
+ public override LocalisableString Description => @"Play with four keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs
index e9a9bba5bd..693182a952 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey5 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 5;
public override string Name => "Five Keys";
public override string Acronym => "5K";
- public override string Description => @"Play with five keys.";
+ public override LocalisableString Description => @"Play with five keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs
index b9606d1cb5..ab911292f7 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey6 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 6;
public override string Name => "Six Keys";
public override string Acronym => "6K";
- public override string Description => @"Play with six keys.";
+ public override LocalisableString Description => @"Play with six keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs
index b80d794085..ab401ef1d0 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey7 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 7;
public override string Name => "Seven Keys";
public override string Acronym => "7K";
- public override string Description => @"Play with seven keys.";
+ public override LocalisableString Description => @"Play with seven keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs
index 3462d634a4..b3e8a45dda 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey8 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 8;
public override string Name => "Eight Keys";
public override string Acronym => "8K";
- public override string Description => @"Play with eight keys.";
+ public override LocalisableString Description => @"Play with eight keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs
index 83c505c048..5972cbf0fe 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModKey9 : ManiaKeyMod
@@ -8,6 +10,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override int KeyCount => 9;
public override string Name => "Nine Keys";
public override string Acronym => "9K";
- public override string Description => @"Play with nine keys.";
+ public override LocalisableString Description => @"Play with nine keys.";
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
index 9c3744ea98..f9690b4298 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs
@@ -5,6 +5,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using System.Linq;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModMirror : ModMirror, IApplicableToBeatmap
{
- public override string Description => "Notes are flipped horizontally.";
+ public override LocalisableString Description => "Notes are flipped horizontally.";
public void ApplyToBeatmap(IBeatmap beatmap)
{
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
index dfb02408d2..6ff070d703 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModRandom : ModRandom, IApplicableToBeatmap
{
- public override string Description => @"Shuffle around the keys!";
+ public override LocalisableString Description => @"Shuffle around the keys!";
public void ApplyToBeatmap(IBeatmap beatmap)
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs
index a72f2031c9..e864afe056 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
@@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
ControlPoints =
{
- new PathControlPoint(Vector2.Zero),
- new PathControlPoint(OsuPlayfield.BASE_SIZE * 2 / 5),
- new PathControlPoint(OsuPlayfield.BASE_SIZE * 3 / 5)
+ new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
+ new PathControlPoint(new Vector2(136, 205)),
+ new PathControlPoint(new Vector2(-4, 226))
}
}
}));
@@ -99,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("move mouse to new point location", () =>
{
var firstPiece = this.ChildrenOfType().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
- var secondPiece = this.ChildrenOfType().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]);
- InputManager.MoveMouseTo((firstPiece.ScreenSpaceDrawQuad.Centre + secondPiece.ScreenSpaceDrawQuad.Centre) / 2);
+ var pos = slider.Path.PositionAt(0.25d) + slider.Position;
+ InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos));
});
AddStep("move slider end", () =>
{
@@ -175,6 +176,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertSliderSnapped(false);
}
+ [Test]
+ public void TestRotatingSliderRetainsPerfectControlPointType()
+ {
+ OsuSelectionHandler selectionHandler;
+
+ AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
+
+ AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
+ AddStep("rotate 90 degrees ccw", () =>
+ {
+ selectionHandler = this.ChildrenOfType().Single();
+ selectionHandler.HandleRotation(-90);
+ });
+
+ AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
+ }
+
[Test]
public void TestFlippingSliderDoesNotSnap()
{
@@ -200,6 +218,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertSliderSnapped(false);
}
+ [Test]
+ public void TestFlippingSliderRetainsPerfectControlPointType()
+ {
+ OsuSelectionHandler selectionHandler;
+
+ AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
+
+ AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
+ AddStep("flip slider horizontally", () =>
+ {
+ selectionHandler = this.ChildrenOfType().Single();
+ selectionHandler.OnPressed(new KeyBindingPressEvent(InputManager.CurrentState, GlobalAction.EditorFlipVertically));
+ });
+
+ AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve);
+ }
+
[Test]
public void TestReversingSliderDoesNotSnap()
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index b7f91c22f4..c01b2576e8 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -199,14 +199,14 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate);
addSeekStep(1000);
- AddAssert("progress almost same", () => expectedProgress, () => Is.EqualTo(drawableSpinner.Progress).Within(0.05));
- AddAssert("spm almost same", () => expectedSpm, () => Is.EqualTo(drawableSpinner.SpinsPerMinute.Value).Within(2.0));
+ AddAssert("progress almost same", () => drawableSpinner.Progress, () => Is.EqualTo(expectedProgress).Within(0.05));
+ AddAssert("spm almost same", () => drawableSpinner.SpinsPerMinute.Value, () => Is.EqualTo(expectedSpm).Within(2.0));
}
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
- AddUntilStep("wait for seek to finish", () => time, () => Is.EqualTo(Player.DrawableRuleset.FrameStableClock.CurrentTime).Within(100));
+ AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100));
}
private void transformReplay(Func replayTransformation) => AddStep("set replay", () =>
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index 22cbab8938..c24f78e430 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -166,8 +166,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
case NotifyCollectionChangedAction.Remove:
foreach (var point in e.OldItems.Cast())
{
- Pieces.RemoveAll(p => p.ControlPoint == point);
- Connections.RemoveAll(c => c.ControlPoint == point);
+ foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
+ piece.RemoveAndDisposeImmediately();
+ foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
+ connection.RemoveAndDisposeImmediately();
}
// If removing before the end of the path,
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index f3c0a05bc2..061c5008c5 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -127,13 +127,16 @@ namespace osu.Game.Rulesets.Osu.Edit
{
didFlip = true;
- foreach (var point in slider.Path.ControlPoints)
- {
- point.Position = new Vector2(
- (direction == Direction.Horizontal ? -1 : 1) * point.Position.X,
- (direction == Direction.Vertical ? -1 : 1) * point.Position.Y
- );
- }
+ var controlPoints = slider.Path.ControlPoints.Select(p =>
+ new PathControlPoint(new Vector2(
+ (direction == Direction.Horizontal ? -1 : 1) * p.Position.X,
+ (direction == Direction.Vertical ? -1 : 1) * p.Position.Y
+ ), p.Type)).ToArray();
+
+ // Importantly, update as a single operation so automatic adjustment of control points to different
+ // curve types does not unexpectedly trigger and change the slider's shape.
+ slider.Path.ControlPoints.Clear();
+ slider.Path.ControlPoints.AddRange(controlPoints);
}
}
@@ -183,8 +186,13 @@ namespace osu.Game.Rulesets.Osu.Edit
if (h is IHasPath path)
{
- foreach (var point in path.Path.ControlPoints)
- point.Position = RotatePointAroundOrigin(point.Position, Vector2.Zero, delta);
+ var controlPoints = path.Path.ControlPoints.Select(p =>
+ new PathControlPoint(RotatePointAroundOrigin(p.Position, Vector2.Zero, delta), p.Type)).ToArray();
+
+ // Importantly, update as a single operation so automatic adjustment of control points to different
+ // curve types does not unexpectedly trigger and change the slider's shape.
+ path.Path.ControlPoints.Clear();
+ path.Path.ControlPoints.AddRange(controlPoints);
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
index d88cb17e84..9bf5d33d4a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
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 LocalisableString Description => @"Don't use the same key twice in a row!";
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
index e6889403a3..ec93f19e17 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
- public override string Description => "Never trust the approach circles...";
+ public override LocalisableString Description => "Never trust the approach circles...";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index 9229c0393d..6772cfe0be 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.StateChanges;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "AP";
public override IconUsage? Icon => OsuIcon.ModAutopilot;
public override ModType Type => ModType.Automation;
- public override string Description => @"Automatic cursor movement - just follow the rhythm.";
+ public override LocalisableString Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
@@ -30,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Mods
private OsuInputManager inputManager = null!;
- private IFrameStableClock gameplayClock = null!;
-
private List replayFrames = null!;
private int currentFrame;
@@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (currentFrame == replayFrames.Count - 1) return;
- double time = gameplayClock.CurrentTime;
+ double time = playfield.Clock.CurrentTime;
// Very naive implementation of autopilot based on proximity to replay frames.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
@@ -55,8 +54,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- gameplayClock = drawableRuleset.FrameStableClock;
-
// Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index 56665db770..4c72667f15 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModBlinds : Mod, IApplicableToDrawableRuleset, IApplicableToHealthProcessor
{
public override string Name => "Blinds";
- public override string Description => "Play with blinds on your screen.";
+ public override LocalisableString Description => "Play with blinds on your screen.";
public override string Acronym => "BL";
public override IconUsage? Icon => FontAwesome.Solid.Adjust;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
index ee6a7815e2..e624660410 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
@@ -3,6 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
@@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.CompressArrowsAlt;
- public override string Description => "Hit them at the right size!";
+ public override LocalisableString Description => "Hit them at the right size!";
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber StartScale { get; } = new BindableFloat
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs
index 06b5b6cfb8..281b36e70e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs
@@ -1,12 +1,13 @@
// 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.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModEasy : ModEasyWithExtraLives
{
- public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
+ public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
index 182d6eeb4b..b77c887cd3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
@@ -3,6 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
@@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAltV;
- public override string Description => "Hit them at the right size!";
+ public override LocalisableString Description => "Hit them at the right size!";
[SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public override BindableNumber StartScale { get; } = new BindableFloat
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 97f201b2cc..996ee1cddb 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")]
public Bindable OnlyFadeApproachCircles { get; } = new BindableBool();
- public override string Description => @"Play with no approach circles and fading circles/sliders.";
+ public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index 7f7d6f70d2..fbde9e0491 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -4,6 +4,8 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@@ -22,12 +24,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "MG";
public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun;
- public override string Description => "No need to chase the circles – your cursor is a magnet!";
+ public override LocalisableString Description => "No need to chase the circles – your cursor is a magnet!";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
- private IFrameStableClock gameplayClock = null!;
-
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{
@@ -38,8 +38,6 @@ namespace osu.Game.Rulesets.Osu.Mods
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;
@@ -55,27 +53,27 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableHitCircle circle:
- easeTo(circle, cursorPos);
+ easeTo(playfield.Clock, circle, cursorPos);
break;
case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult)
- easeTo(slider, cursorPos);
+ easeTo(playfield.Clock, slider, cursorPos);
else
- easeTo(slider, cursorPos - slider.Ball.DrawPosition);
+ easeTo(playfield.Clock, slider, cursorPos - slider.Ball.DrawPosition);
break;
}
}
}
- private void easeTo(DrawableHitObject hitObject, Vector2 destination)
+ private void easeTo(IFrameBasedClock clock, DrawableHitObject hitObject, Vector2 destination)
{
double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
- float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
- float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
+ float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime);
+ float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.ElapsedFrameTime);
hitObject.Position = new Vector2(x, y);
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
index 3faca0b01f..0a54d58718 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMirror.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModMirror : ModMirror, IApplicableToHitObject
{
- public override string Description => "Flip objects on the chosen axes.";
+ public override LocalisableString Description => "Flip objects on the chosen axes.";
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")]
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
index 3eb8982f5d..817f7b599c 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap
{
- public override string Description => "Where's the cursor?";
+ public override LocalisableString Description => "Where's the cursor?";
private PeriodTracker spinnerPeriods = null!;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 4f83154728..96c02a508b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
///
public class OsuModRandom : ModRandom, IApplicableToBeatmap
{
- public override string Description => "It never gets boring!";
+ public override LocalisableString Description => "It never gets boring!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 908bb34ed6..fac1cbfd47 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
@@ -18,7 +19,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 LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
///
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
index 211987ee32..911363a27e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
@@ -2,8 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Diagnostics;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
+using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@@ -22,12 +23,10 @@ namespace osu.Game.Rulesets.Osu.Mods
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 LocalisableString 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)
{
@@ -38,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.Mods
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;
@@ -68,29 +65,27 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableHitCircle circle:
- easeTo(circle, destination, cursorPos);
+ easeTo(playfield.Clock, circle, destination, cursorPos);
break;
case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult)
- easeTo(slider, destination, cursorPos);
+ easeTo(playfield.Clock, slider, destination, cursorPos);
else
- easeTo(slider, destination - slider.Ball.DrawPosition, cursorPos);
+ easeTo(playfield.Clock, slider, destination - slider.Ball.DrawPosition, cursorPos);
break;
}
}
}
- private void easeTo(DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
+ private void easeTo(IFrameBasedClock clock, 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);
+ float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, clock.ElapsedFrameTime);
+ float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, clock.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
index b170d30448..91731b25cf 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => @"Single Tap";
public override string Acronym => @"SG";
- public override string Description => @"You must only use one key!";
+ public override LocalisableString 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/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
index 95e7d13ee7..b0533d0cfa 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "SI";
public override IconUsage? Icon => FontAwesome.Solid.Undo;
public override ModType Type => ModType.Fun;
- public override string Description => "Circles spin in. No approach circles.";
+ public override LocalisableString Description => "Circles spin in. No approach circles.";
public override double ScoreMultiplier => 1;
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index d9ab749ad3..9708800daa 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "SO";
public override IconUsage? Icon => OsuIcon.ModSpunOut;
public override ModType Type => ModType.Automation;
- public override string Description => @"Spinners will be automatically completed.";
+ public override LocalisableString Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
index 0b34ab28a3..67b19124e1 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Threading;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => @"Strict Tracking";
public override string Acronym => @"ST";
public override ModType Type => ModType.DifficultyIncrease;
- public override string Description => @"Once you start a slider, follow precisely or get a miss.";
+ public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
index 623157a427..82260db818 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -39,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "TP";
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => OsuIcon.ModTarget;
- public override string Description => @"Practice keeping up with the beat of the song.";
+ public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
index 7276cc753c..fd5c46a226 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
@@ -9,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Touch Device";
public override string Acronym => "TD";
- public override string Description => "Automatically applied to plays on devices with a touchscreen.";
+ public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index d862d36670..25d05a88a8 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => "Traceable";
public override string Acronym => "TC";
public override ModType Type => ModType.Fun;
- public override string Description => "Put your faith in the approach circles...";
+ public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
index 4354ecbe9a..2354cd50ae 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "TR";
public override IconUsage? Icon => FontAwesome.Solid.ArrowsAlt;
public override ModType Type => ModType.Fun;
- public override string Description => "Everything rotates. EVERYTHING.";
+ public override LocalisableString Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index 3f1c3aa812..a45338d91f 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => "WG";
public override IconUsage? Icon => FontAwesome.Solid.Certificate;
public override ModType Type => ModType.Fun;
- public override string Description => "They just won't stay still...";
+ public override LocalisableString Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 302194e91a..7f58f29d4b 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -32,6 +32,7 @@ using osu.Game.Skinning;
using System;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Osu.Edit.Setup;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
@@ -253,7 +254,7 @@ namespace osu.Game.Rulesets.Osu
};
}
- public override string GetDisplayNameForHitResult(HitResult result)
+ public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
index fc3f89a836..412505331b 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osuTK;
@@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI
public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
- protected override string Message => "Click the orange cursor to resume";
+ protected override LocalisableString Message => "Click the orange cursor to resume";
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
index ef95358d34..9163f994c5 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.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.Collections.Generic;
using System.Linq;
using Humanizer;
@@ -36,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
TimeRange = { Value = 5000 },
};
- private TaikoScoreProcessor scoreProcessor;
+ private TaikoScoreProcessor scoreProcessor = null!;
private IEnumerable mascots => this.ChildrenOfType();
@@ -65,6 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestInitialState()
{
+ AddStep("set beatmap", () => setBeatmap());
+
AddStep("create mascot", () => SetContents(_ => new DrawableTaikoMascot { RelativeSizeAxes = Axes.Both }));
AddAssert("mascot initially idle", () => allMascotsIn(TaikoMascotAnimationState.Idle));
@@ -89,9 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestIdleState()
{
- AddStep("set beatmap", () => setBeatmap());
-
- createDrawableRuleset();
+ prepareDrawableRulesetAndBeatmap(false);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle);
@@ -100,9 +98,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestKiaiState()
{
- AddStep("set beatmap", () => setBeatmap(true));
-
- createDrawableRuleset();
+ prepareDrawableRulesetAndBeatmap(true);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Kiai);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai);
@@ -112,9 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[Test]
public void TestMissState()
{
- AddStep("set beatmap", () => setBeatmap());
-
- createDrawableRuleset();
+ prepareDrawableRulesetAndBeatmap(false);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
@@ -126,9 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestCase(false)]
public void TestClearStateOnComboMilestone(bool kiai)
{
- AddStep("set beatmap", () => setBeatmap(kiai));
-
- createDrawableRuleset();
+ prepareDrawableRulesetAndBeatmap(kiai);
AddRepeatStep("reach 49 combo", () => applyNewResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }), 49);
@@ -139,9 +131,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestCase(false, TaikoMascotAnimationState.Idle)]
public void TestClearStateOnClearedSwell(bool kiai, TaikoMascotAnimationState expectedStateAfterClear)
{
- AddStep("set beatmap", () => setBeatmap(kiai));
-
- createDrawableRuleset();
+ prepareDrawableRulesetAndBeatmap(kiai);
assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear);
AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLowerInvariant()}", () => allMascotsIn(expectedStateAfterClear));
@@ -175,25 +165,27 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
scoreProcessor.ApplyBeatmap(Beatmap.Value.Beatmap);
}
- private void createDrawableRuleset()
+ private void prepareDrawableRulesetAndBeatmap(bool kiai)
{
- AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded);
+ AddStep("set beatmap", () => setBeatmap(kiai));
AddStep("create drawable ruleset", () =>
{
- Beatmap.Value.Track.Start();
-
SetContents(_ =>
{
var ruleset = new TaikoRuleset();
return new DrawableTaikoRuleset(ruleset, Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo));
});
});
+
+ AddUntilStep("wait for track to be loaded", () => MusicController.TrackLoaded);
+ AddStep("start track", () => MusicController.CurrentTrack.Restart());
+ AddUntilStep("wait for track started", () => MusicController.IsPlaying);
}
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
{
- TaikoMascotAnimationState[] mascotStates = null;
+ TaikoMascotAnimationState[] mascotStates = null!;
AddStep($"{judgementResult.Type.ToString().ToLowerInvariant()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
() =>
@@ -204,7 +196,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
});
- AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.All(state => state == expectedState));
+ AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.Distinct(), () => Is.EquivalentTo(new[] { expectedState }));
}
private void applyNewResult(JudgementResult judgementResult)
diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
index 579b461624..425f72cadc 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs
@@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
- [TestCase(1.9971301024093662d, 200, "diffcalc-test")]
- [TestCase(1.9971301024093662d, 200, "diffcalc-test-strong")]
+ [TestCase(3.1098944660126882d, 200, "diffcalc-test")]
+ [TestCase(3.1098944660126882d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(3.1645810961313674d, 200, "diffcalc-test")]
- [TestCase(3.1645810961313674d, 200, "diffcalc-test-strong")]
+ [TestCase(4.0974106752474251d, 200, "diffcalc-test")]
+ [TestCase(4.0974106752474251d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
new file mode 100644
index 0000000000..7d88be2f70
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
@@ -0,0 +1,67 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
+{
+ public class ColourEvaluator
+ {
+ ///
+ /// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2).
+ ///
+ /// The input value.
+ /// The center of the sigmoid, where the largest gradient occurs and value is equal to middle.
+ /// The radius of the sigmoid, outside of which values are near the minimum/maximum.
+ /// The middle of the sigmoid output.
+ /// The height of the sigmoid output. This will be equal to max value - min value.
+ private static double sigmoid(double val, double center, double width, double middle, double height)
+ {
+ double sigmoid = Math.Tanh(Math.E * -(val - center) / width);
+ return sigmoid * (height / 2) + middle;
+ }
+
+ ///
+ /// Evaluate the difficulty of the first note of a .
+ ///
+ public static double EvaluateDifficultyOf(MonoStreak monoStreak)
+ {
+ return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
+ }
+
+ ///
+ /// Evaluate the difficulty of the first note of a .
+ ///
+ public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
+ {
+ return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
+ }
+
+ ///
+ /// Evaluate the difficulty of the first note of a .
+ ///
+ public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
+ {
+ return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1));
+ }
+
+ public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
+ {
+ TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
+ double difficulty = 0.0d;
+
+ if (colour.MonoStreak != null) // Difficulty for MonoStreak
+ difficulty += EvaluateDifficultyOf(colour.MonoStreak);
+ if (colour.AlternatingMonoPattern != null) // Difficulty for AlternatingMonoPattern
+ difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
+ if (colour.RepeatingHitPattern != null) // Difficulty for RepeatingHitPattern
+ difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
+
+ return difficulty;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
new file mode 100644
index 0000000000..49b3ae2e19
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
+{
+ public class StaminaEvaluator
+ {
+ ///
+ /// Applies a speed bonus dependent on the time since the last hit performed using this key.
+ ///
+ /// The interval between the current and previous note hit using the same key.
+ private static double speedBonus(double interval)
+ {
+ // Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
+ // Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
+ // TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
+ interval = Math.Max(interval, 50);
+
+ return 30 / interval;
+ }
+
+ ///
+ /// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
+ /// maximum possible interval between two hits using the same key, by alternating 2 keys for each colour.
+ ///
+ public static double EvaluateDifficultyOf(DifficultyHitObject current)
+ {
+ if (current.BaseObject is not Hit)
+ {
+ return 0.0;
+ }
+
+ // Find the previous hit object hit by the current key, which is two notes of the same colour prior.
+ TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
+ TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(1);
+
+ if (keyPrevious == null)
+ {
+ // There is no previous hit object hit by the current key
+ return 0.0;
+ }
+
+ double objectStrain = 0.5; // Add a base strain to all objects
+ objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime);
+ return objectStrain;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs
new file mode 100644
index 0000000000..7910a8262b
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/AlternatingMonoPattern.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
+{
+ ///
+ /// Encodes a list of s.
+ /// s with the same are grouped together.
+ ///
+ public class AlternatingMonoPattern
+ {
+ ///
+ /// s that are grouped together within this .
+ ///
+ public readonly List MonoStreaks = new List();
+
+ ///
+ /// The parent that contains this
+ ///
+ public RepeatingHitPatterns Parent = null!;
+
+ ///
+ /// Index of this within it's parent
+ ///
+ public int Index;
+
+ ///
+ /// The first in this .
+ ///
+ public TaikoDifficultyHitObject FirstHitObject => MonoStreaks[0].FirstHitObject;
+
+ ///
+ /// Determine if this is a repetition of another . This
+ /// is a strict comparison and is true if and only if the colour sequence is exactly the same.
+ ///
+ public bool IsRepetitionOf(AlternatingMonoPattern other)
+ {
+ return HasIdenticalMonoLength(other) &&
+ other.MonoStreaks.Count == MonoStreaks.Count &&
+ other.MonoStreaks[0].HitType == MonoStreaks[0].HitType;
+ }
+
+ ///
+ /// Determine if this has the same mono length of another .
+ ///
+ public bool HasIdenticalMonoLength(AlternatingMonoPattern other)
+ {
+ return other.MonoStreaks[0].RunLength == MonoStreaks[0].RunLength;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs
new file mode 100644
index 0000000000..174988bed7
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/MonoStreak.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Objects;
+using System.Collections.Generic;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
+{
+ ///
+ /// Encode colour information for a sequence of s. Consecutive s
+ /// of the same are encoded within the same .
+ ///
+ public class MonoStreak
+ {
+ ///
+ /// List of s that are encoded within this .
+ ///
+ public List HitObjects { get; private set; } = new List();
+
+ ///
+ /// The parent that contains this
+ ///
+ public AlternatingMonoPattern Parent = null!;
+
+ ///
+ /// Index of this within it's parent
+ ///
+ public int Index;
+
+ ///
+ /// The first in this .
+ ///
+ public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
+
+ ///
+ /// The hit type of all objects encoded within this
+ ///
+ public HitType? HitType => (HitObjects[0].BaseObject as Hit)?.Type;
+
+ ///
+ /// How long the mono pattern encoded within is
+ ///
+ public int RunLength => HitObjects.Count;
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs
new file mode 100644
index 0000000000..fe0dc6dd9a
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/Data/RepeatingHitPatterns.cs
@@ -0,0 +1,94 @@
+// 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;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
+{
+ ///
+ /// Encodes a list of s, grouped together by back and forth repetition of the same
+ /// . Also stores the repetition interval between this and the previous .
+ ///
+ public class RepeatingHitPatterns
+ {
+ ///
+ /// Maximum amount of s to look back to find a repetition.
+ ///
+ private const int max_repetition_interval = 16;
+
+ ///
+ /// The s that are grouped together within this .
+ ///
+ public readonly List AlternatingMonoPatterns = new List();
+
+ ///
+ /// The parent in this
+ ///
+ public TaikoDifficultyHitObject FirstHitObject => AlternatingMonoPatterns[0].FirstHitObject;
+
+ ///
+ /// The previous . This is used to determine the repetition interval.
+ ///
+ public readonly RepeatingHitPatterns? Previous;
+
+ ///
+ /// How many between the current and previous identical .
+ /// If no repetition is found this will have a value of + 1.
+ ///
+ public int RepetitionInterval { get; private set; } = max_repetition_interval + 1;
+
+ public RepeatingHitPatterns(RepeatingHitPatterns? previous)
+ {
+ Previous = previous;
+ }
+
+ ///
+ /// Returns true if other is considered a repetition of this pattern. This is true if other's first two payloads
+ /// have identical mono lengths.
+ ///
+ private bool isRepetitionOf(RepeatingHitPatterns other)
+ {
+ if (AlternatingMonoPatterns.Count != other.AlternatingMonoPatterns.Count) return false;
+
+ for (int i = 0; i < Math.Min(AlternatingMonoPatterns.Count, 2); i++)
+ {
+ if (!AlternatingMonoPatterns[i].HasIdenticalMonoLength(other.AlternatingMonoPatterns[i])) return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Finds the closest previous that has the identical .
+ /// Interval is defined as the amount of chunks between the current and repeated patterns.
+ ///
+ public void FindRepetitionInterval()
+ {
+ if (Previous == null)
+ {
+ RepetitionInterval = max_repetition_interval + 1;
+ return;
+ }
+
+ RepeatingHitPatterns? other = Previous;
+ int interval = 1;
+
+ while (interval < max_repetition_interval)
+ {
+ if (isRepetitionOf(other))
+ {
+ RepetitionInterval = Math.Min(interval, max_repetition_interval);
+ return;
+ }
+
+ other = other.Previous;
+ if (other == null) break;
+
+ ++interval;
+ }
+
+ RepetitionInterval = max_repetition_interval + 1;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs
new file mode 100644
index 0000000000..d19e05f4e0
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoColourDifficultyPreprocessor.cs
@@ -0,0 +1,167 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
+{
+ ///
+ /// Utility class to perform various encodings.
+ ///
+ public static class TaikoColourDifficultyPreprocessor
+ {
+ ///
+ /// Processes and encodes a list of s into a list of s,
+ /// assigning the appropriate s to each ,
+ /// and pre-evaluating colour difficulty of each .
+ ///
+ public static void ProcessAndAssign(List hitObjects)
+ {
+ List hitPatterns = encode(hitObjects);
+
+ // Assign indexing and encoding data to all relevant objects. Only the first note of each encoding type is
+ // assigned with the relevant encodings.
+ foreach (var repeatingHitPattern in hitPatterns)
+ {
+ repeatingHitPattern.FirstHitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
+
+ // The outermost loop is kept a ForEach loop since it doesn't need index information, and we want to
+ // keep i and j for AlternatingMonoPattern's and MonoStreak's index respectively, to keep it in line with
+ // documentation.
+ for (int i = 0; i < repeatingHitPattern.AlternatingMonoPatterns.Count; ++i)
+ {
+ AlternatingMonoPattern monoPattern = repeatingHitPattern.AlternatingMonoPatterns[i];
+ monoPattern.Parent = repeatingHitPattern;
+ monoPattern.Index = i;
+ monoPattern.FirstHitObject.Colour.AlternatingMonoPattern = monoPattern;
+
+ for (int j = 0; j < monoPattern.MonoStreaks.Count; ++j)
+ {
+ MonoStreak monoStreak = monoPattern.MonoStreaks[j];
+ monoStreak.Parent = monoPattern;
+ monoStreak.Index = j;
+ monoStreak.FirstHitObject.Colour.MonoStreak = monoStreak;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Encodes a list of s into a list of s.
+ ///
+ private static List encode(List data)
+ {
+ List monoStreaks = encodeMonoStreak(data);
+ List alternatingMonoPatterns = encodeAlternatingMonoPattern(monoStreaks);
+ List repeatingHitPatterns = encodeRepeatingHitPattern(alternatingMonoPatterns);
+
+ return repeatingHitPatterns;
+ }
+
+ ///
+ /// Encodes a list of s into a list of s.
+ ///
+ private static List encodeMonoStreak(List data)
+ {
+ List monoStreaks = new List();
+ MonoStreak? currentMonoStreak = null;
+
+ for (int i = 0; i < data.Count; i++)
+ {
+ TaikoDifficultyHitObject taikoObject = (TaikoDifficultyHitObject)data[i];
+
+ // This ignores all non-note objects, which may or may not be the desired behaviour
+ TaikoDifficultyHitObject? previousObject = taikoObject.PreviousNote(0);
+
+ // If this is the first object in the list or the colour changed, create a new mono streak
+ if (currentMonoStreak == null || previousObject == null || (taikoObject.BaseObject as Hit)?.Type != (previousObject.BaseObject as Hit)?.Type)
+ {
+ currentMonoStreak = new MonoStreak();
+ monoStreaks.Add(currentMonoStreak);
+ }
+
+ // Add the current object to the encoded payload.
+ currentMonoStreak.HitObjects.Add(taikoObject);
+ }
+
+ return monoStreaks;
+ }
+
+ ///
+ /// Encodes a list of s into a list of s.
+ ///
+ private static List encodeAlternatingMonoPattern(List data)
+ {
+ List monoPatterns = new List();
+ AlternatingMonoPattern? currentMonoPattern = null;
+
+ for (int i = 0; i < data.Count; i++)
+ {
+ // Start a new AlternatingMonoPattern if the previous MonoStreak has a different mono length, or if this is the first MonoStreak in the list.
+ if (currentMonoPattern == null || data[i].RunLength != data[i - 1].RunLength)
+ {
+ currentMonoPattern = new AlternatingMonoPattern();
+ monoPatterns.Add(currentMonoPattern);
+ }
+
+ // Add the current MonoStreak to the encoded payload.
+ currentMonoPattern.MonoStreaks.Add(data[i]);
+ }
+
+ return monoPatterns;
+ }
+
+ ///
+ /// Encodes a list of s into a list of s.
+ ///
+ private static List encodeRepeatingHitPattern(List data)
+ {
+ List hitPatterns = new List();
+ RepeatingHitPatterns? currentHitPattern = null;
+
+ for (int i = 0; i < data.Count; i++)
+ {
+ // Start a new RepeatingHitPattern. AlternatingMonoPatterns that should be grouped together will be handled later within this loop.
+ currentHitPattern = new RepeatingHitPatterns(currentHitPattern);
+
+ // Determine if future AlternatingMonoPatterns should be grouped.
+ bool isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]);
+
+ if (!isCoupled)
+ {
+ // If not, add the current AlternatingMonoPattern to the encoded payload and continue.
+ currentHitPattern.AlternatingMonoPatterns.Add(data[i]);
+ }
+ else
+ {
+ // If so, add the current AlternatingMonoPattern to the encoded payload and start repeatedly checking if the
+ // subsequent AlternatingMonoPatterns should be grouped by increasing i and doing the appropriate isCoupled check.
+ while (isCoupled)
+ {
+ currentHitPattern.AlternatingMonoPatterns.Add(data[i]);
+ i++;
+ isCoupled = i < data.Count - 2 && data[i].IsRepetitionOf(data[i + 2]);
+ }
+
+ // Skip over viewed data and add the rest to the payload
+ currentHitPattern.AlternatingMonoPatterns.Add(data[i]);
+ currentHitPattern.AlternatingMonoPatterns.Add(data[i + 1]);
+ i++;
+ }
+
+ hitPatterns.Add(currentHitPattern);
+ }
+
+ // Final pass to find repetition intervals
+ for (int i = 0; i < hitPatterns.Count; i++)
+ {
+ hitPatterns[i].FindRepetitionInterval();
+ }
+
+ return hitPatterns;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs
new file mode 100644
index 0000000000..9c147eee9c
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Colour/TaikoDifficultyHitObjectColour.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
+{
+ ///
+ /// Stores colour compression information for a .
+ ///
+ public class TaikoDifficultyHitObjectColour
+ {
+ ///
+ /// The that encodes this note, only present if this is the first note within a
+ ///
+ public MonoStreak? MonoStreak;
+
+ ///
+ /// The that encodes this note, only present if this is the first note within a
+ ///
+ public AlternatingMonoPattern? AlternatingMonoPattern;
+
+ ///
+ /// The that encodes this note, only present if this is the first note within a
+ ///
+ public RepeatingHitPatterns? RepeatingHitPattern;
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
similarity index 95%
rename from osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs
rename to osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
index 526d20e7d7..a273d7e2ea 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Rhythm/TaikoDifficultyHitObjectRhythm.cs
@@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
+namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
{
///
/// Represents a rhythm change in a taiko map.
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
index 4c7b140832..4aaee50c18 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs
@@ -1,14 +1,14 @@
// 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.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
@@ -17,21 +17,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
///
public class TaikoDifficultyHitObject : DifficultyHitObject
{
+ ///
+ /// The list of all of the same colour as this in the beatmap.
+ ///
+ private readonly IReadOnlyList? monoDifficultyHitObjects;
+
+ ///
+ /// The index of this in .
+ ///
+ public readonly int MonoIndex;
+
+ ///
+ /// The list of all that is either a regular note or finisher in the beatmap
+ ///
+ private readonly IReadOnlyList noteDifficultyHitObjects;
+
+ ///
+ /// The index of this in .
+ ///
+ public readonly int NoteIndex;
+
///
/// The rhythm required to hit this hit object.
///
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
///
- /// The hit type of this hit object.
+ /// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used
+ /// by other skills in the future.
///
- public readonly HitType? HitType;
-
- ///
- /// Whether the object should carry a penalty due to being hittable using special techniques
- /// making it easier to do so.
- ///
- public bool StaminaCheese;
+ public readonly TaikoDifficultyHitObjectColour Colour;
///
/// Creates a new difficulty hit object.
@@ -40,15 +55,44 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
/// The gameplay preceding .
/// The gameplay preceding .
/// The rate of the gameplay clock. Modified by speed-changing mods.
- /// The list of s in the current beatmap.
- /// /// The position of this in the list.
- public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List objects, int index)
+ /// The list of all s in the current beatmap.
+ /// The list of centre (don) s in the current beatmap.
+ /// The list of rim (kat) s in the current beatmap.
+ /// The list of s that is a hit (i.e. not a drumroll or swell) in the current beatmap.
+ /// The position of this in the list.
+ public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate,
+ List objects,
+ List centreHitObjects,
+ List rimHitObjects,
+ List noteObjects, int index)
: base(hitObject, lastObject, clockRate, objects, index)
{
- var currentHit = hitObject as Hit;
+ noteDifficultyHitObjects = noteObjects;
+ // Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
+ Colour = new TaikoDifficultyHitObjectColour();
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
- HitType = currentHit?.Type;
+
+ switch ((hitObject as Hit)?.Type)
+ {
+ case HitType.Centre:
+ MonoIndex = centreHitObjects.Count;
+ centreHitObjects.Add(this);
+ monoDifficultyHitObjects = centreHitObjects;
+ break;
+
+ case HitType.Rim:
+ MonoIndex = rimHitObjects.Count;
+ rimHitObjects.Add(this);
+ monoDifficultyHitObjects = rimHitObjects;
+ break;
+ }
+
+ if (hitObject is Hit)
+ {
+ NoteIndex = noteObjects.Count;
+ noteObjects.Add(this);
+ }
}
///
@@ -87,5 +131,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
}
+
+ public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1));
+
+ public TaikoDifficultyHitObject? NextMono(int forwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex + (forwardsIndex + 1));
+
+ public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1));
+
+ public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1));
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
index 3727c0e4b7..2d45b5eed0 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
@@ -1,15 +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;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
-using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
@@ -18,29 +13,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
public class Colour : StrainDecaySkill
{
- protected override double SkillMultiplier => 1;
- protected override double StrainDecayBase => 0.4;
+ protected override double SkillMultiplier => 0.12;
- ///
- /// Maximum number of entries to keep in .
- ///
- private const int mono_history_max_length = 5;
-
- ///
- /// Queue with the lengths of the last most recent mono (single-colour) patterns,
- /// with the most recent value at the end of the queue.
- ///
- private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length);
-
- ///
- /// The of the last object hit before the one being considered.
- ///
- private HitType? previousHitType;
-
- ///
- /// Length of the current mono pattern.
- ///
- private int currentMonoLength;
+ // This is set to decay slower than other skills, due to the fact that only the first note of each encoding class
+ // having any difficulty values, and we want to allow colour difficulty to be able to build up even on
+ // slower maps.
+ protected override double StrainDecayBase => 0.8;
public Colour(Mod[] mods)
: base(mods)
@@ -49,95 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
- // changing from/to a drum roll or a swell does not constitute a colour change.
- // hits spaced more than a second apart are also exempt from colour strain.
- if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000))
- {
- monoHistory.Clear();
-
- var currentHit = current.BaseObject as Hit;
- currentMonoLength = currentHit != null ? 1 : 0;
- previousHitType = currentHit?.Type;
-
- return 0.0;
- }
-
- var taikoCurrent = (TaikoDifficultyHitObject)current;
-
- double objectStrain = 0.0;
-
- if (previousHitType != null && taikoCurrent.HitType != previousHitType)
- {
- // The colour has changed.
- objectStrain = 1.0;
-
- if (monoHistory.Count < 2)
- {
- // There needs to be at least two streaks to determine a strain.
- objectStrain = 0.0;
- }
- else if ((monoHistory[^1] + currentMonoLength) % 2 == 0)
- {
- // The last streak in the history is guaranteed to be a different type to the current streak.
- // If the total number of notes in the two streaks is even, nullify this object's strain.
- objectStrain = 0.0;
- }
-
- objectStrain *= repetitionPenalties();
- currentMonoLength = 1;
- }
- else
- {
- currentMonoLength += 1;
- }
-
- previousHitType = taikoCurrent.HitType;
- return objectStrain;
+ return ColourEvaluator.EvaluateDifficultyOf(current);
}
-
- ///
- /// The penalty to apply due to the length of repetition in colour streaks.
- ///
- private double repetitionPenalties()
- {
- const int most_recent_patterns_to_compare = 2;
- double penalty = 1.0;
-
- monoHistory.Enqueue(currentMonoLength);
-
- for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--)
- {
- if (!isSamePattern(start, most_recent_patterns_to_compare))
- continue;
-
- int notesSince = 0;
- for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i];
- penalty *= repetitionPenalty(notesSince);
- break;
- }
-
- return penalty;
- }
-
- ///
- /// Determines whether the last patterns have repeated in the history
- /// of single-colour note sequences, starting from .
- ///
- private bool isSamePattern(int start, int mostRecentPatternsToCompare)
- {
- for (int i = 0; i < mostRecentPatternsToCompare; i++)
- {
- if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i])
- return false;
- }
-
- return true;
- }
-
- ///
- /// Calculates the strain penalty for a colour pattern repetition.
- ///
- /// The number of notes since the last repetition of the pattern.
- private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs
new file mode 100644
index 0000000000..ec8e754c5c
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Peaks.cs
@@ -0,0 +1,93 @@
+// 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.Game.Rulesets.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Difficulty.Skills;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
+{
+ public class Peaks : Skill
+ {
+ private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
+ private const double colour_skill_multiplier = 0.375 * final_multiplier;
+ private const double stamina_skill_multiplier = 0.375 * final_multiplier;
+
+ private const double final_multiplier = 0.0625;
+
+ private readonly Rhythm rhythm;
+ private readonly Colour colour;
+ private readonly Stamina stamina;
+
+ public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier;
+ public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier;
+ public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier;
+
+ public Peaks(Mod[] mods)
+ : base(mods)
+ {
+ rhythm = new Rhythm(mods);
+ colour = new Colour(mods);
+ stamina = new Stamina(mods);
+ }
+
+ ///
+ /// Returns the p-norm of an n-dimensional vector.
+ ///
+ /// The value of p to calculate the norm for.
+ /// The coefficients of the vector.
+ private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
+
+ public override void Process(DifficultyHitObject current)
+ {
+ rhythm.Process(current);
+ colour.Process(current);
+ stamina.Process(current);
+ }
+
+ ///
+ /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
+ ///
+ ///
+ /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
+ /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
+ ///
+ public override double DifficultyValue()
+ {
+ List peaks = new List();
+
+ var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
+ var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
+ var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
+
+ for (int i = 0; i < colourPeaks.Count; i++)
+ {
+ double colourPeak = colourPeaks[i] * colour_skill_multiplier;
+ double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
+ double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
+
+ double peak = norm(1.5, colourPeak, staminaPeak);
+ peak = norm(2, peak, rhythmPeak);
+
+ // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
+ // These sections will not contribute to the difficulty.
+ if (peak > 0)
+ peaks.Add(peak);
+ }
+
+ double difficulty = 0;
+ double weight = 1;
+
+ foreach (double strain in peaks.OrderByDescending(d => d))
+ {
+ difficulty += strain * weight;
+ weight *= 0.9;
+ }
+
+ return difficulty;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs
deleted file mode 100644
index c2e1dd4f82..0000000000
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SingleKeyStamina.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using osu.Game.Rulesets.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Difficulty.Skills;
-
-namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
-{
- ///
- /// Stamina of a single key, calculated based on repetition speed.
- ///
- public class SingleKeyStamina
- {
- private double? previousHitTime;
-
- ///
- /// Similar to
- ///
- public double StrainValueOf(DifficultyHitObject current)
- {
- if (previousHitTime == null)
- {
- previousHitTime = current.StartTime;
- return 0;
- }
-
- double objectStrain = 0.5;
- objectStrain += speedBonus(current.StartTime - previousHitTime.Value);
- previousHitTime = current.StartTime;
- return objectStrain;
- }
-
- ///
- /// Applies a speed bonus dependent on the time since the last hit performed using this key.
- ///
- /// The duration between the current and previous note hit using the same key.
- private double speedBonus(double notePairDuration)
- {
- return 175 / (notePairDuration + 100);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
index 67b628a814..344004bcf6 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
@@ -6,8 +6,7 @@
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
-using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
@@ -19,31 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
///
public class Stamina : StrainDecaySkill
{
- protected override double SkillMultiplier => 1;
+ protected override double SkillMultiplier => 1.1;
protected override double StrainDecayBase => 0.4;
- private readonly SingleKeyStamina[] centreKeyStamina =
- {
- new SingleKeyStamina(),
- new SingleKeyStamina()
- };
-
- private readonly SingleKeyStamina[] rimKeyStamina =
- {
- new SingleKeyStamina(),
- new SingleKeyStamina()
- };
-
- ///
- /// Current index into for a centre hit.
- ///
- private int centreKeyIndex;
-
- ///
- /// Current index into for a rim hit.
- ///
- private int rimKeyIndex;
-
///
/// Creates a skill.
///
@@ -53,32 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
}
- ///
- /// Get the next to use for the given .
- ///
- /// The current .
- private SingleKeyStamina getNextSingleKeyStamina(TaikoDifficultyHitObject current)
- {
- // Alternate key for the same color.
- if (current.HitType == HitType.Centre)
- {
- centreKeyIndex = (centreKeyIndex + 1) % 2;
- return centreKeyStamina[centreKeyIndex];
- }
-
- rimKeyIndex = (rimKeyIndex + 1) % 2;
- return rimKeyStamina[rimKeyIndex];
- }
-
protected override double StrainValueOf(DifficultyHitObject current)
{
- if (!(current.BaseObject is Hit))
- {
- return 0.0;
- }
-
- TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
- return getNextSingleKeyStamina(hitObject).StrainValueOf(hitObject);
+ return StaminaEvaluator.EvaluateDifficultyOf(current);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
index 380ab4a4fc..72452e27b3 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
@@ -29,13 +29,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public double ColourDifficulty { get; set; }
///
- /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
+ /// The difficulty corresponding to the hardest parts of the map.
///
- ///
- /// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
- ///
- [JsonProperty("approach_rate")]
- public double ApproachRate { get; set; }
+ [JsonProperty("peak_difficulty")]
+ public double PeakDifficulty { get; set; }
///
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
index 9267d1ee3c..ea2f04a3d9 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
+using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
@@ -22,9 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoDifficultyCalculator : DifficultyCalculator
{
- private const double rhythm_skill_multiplier = 0.014;
- private const double colour_skill_multiplier = 0.01;
- private const double stamina_skill_multiplier = 0.021;
+ private const double difficulty_multiplier = 1.35;
public override int Version => 20220701;
@@ -33,12 +32,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
}
- protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
+ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
- new Colour(mods),
- new Rhythm(mods),
- new Stamina(mods)
- };
+ return new Skill[]
+ {
+ new Peaks(mods)
+ };
+ }
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
@@ -50,18 +50,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
- List taikoDifficultyHitObjects = new List();
+ List difficultyHitObjects = new List();
+ List centreObjects = new List();
+ List rimObjects = new List();
+ List noteObjects = new List();
for (int i = 2; i < beatmap.HitObjects.Count; i++)
{
- taikoDifficultyHitObjects.Add(
+ difficultyHitObjects.Add(
new TaikoDifficultyHitObject(
- beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, taikoDifficultyHitObjects, taikoDifficultyHitObjects.Count
- )
+ beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects,
+ centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count)
);
}
- return taikoDifficultyHitObjects;
+ TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
+
+ return difficultyHitObjects;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@@ -69,28 +74,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods };
- var colour = (Colour)skills[0];
- var rhythm = (Rhythm)skills[1];
- var stamina = (Stamina)skills[2];
+ var combined = (Peaks)skills[0];
- double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
- double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
- double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
+ double colourRating = combined.ColourDifficultyValue * difficulty_multiplier;
+ double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier;
+ double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier;
- double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
- staminaRating *= staminaPenalty;
+ double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
+ double starRating = rescale(combinedRating * 1.4);
- //TODO : This is a temporary fix for the stamina rating of converts, due to their low colour variance.
- if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0 && colourRating < 0.05)
+ // TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
+ if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
{
- staminaPenalty *= 0.25;
+ starRating *= 0.925;
+ // For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
+ if (colourRating < 2 && staminaRating > 8)
+ starRating *= 0.80;
}
- double combinedRating = locallyCombinedDifficulty(colour, rhythm, stamina, staminaPenalty);
- double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
- double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
- starRating = rescale(starRating);
-
HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
@@ -101,75 +102,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
StaminaDifficulty = staminaRating,
RhythmDifficulty = rhythmRating,
ColourDifficulty = colourRating,
+ PeakDifficulty = combinedRating,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
};
}
///
- /// Calculates the penalty for the stamina skill for maps with low colour difficulty.
- ///
- ///
- /// Some maps (especially converts) can be easy to read despite a high note density.
- /// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill.
- ///
- private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty)
- {
- if (colorDifficulty <= 0) return 0.79 - 0.25;
-
- return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2;
- }
-
- ///
- /// Returns the p-norm of an n-dimensional vector.
- ///
- /// The value of p to calculate the norm for.
- /// The coefficients of the vector.
- private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
-
- ///
- /// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map.
- ///
- ///
- /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
- /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
- ///
- private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina stamina, double staminaPenalty)
- {
- List peaks = new List();
-
- var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
- var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
- var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
-
- for (int i = 0; i < colourPeaks.Count; i++)
- {
- double colourPeak = colourPeaks[i] * colour_skill_multiplier;
- double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
- double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * staminaPenalty;
-
- double peak = norm(2, colourPeak, rhythmPeak, staminaPeak);
-
- // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
- // These sections will not contribute to the difficulty.
- if (peak > 0)
- peaks.Add(peak);
- }
-
- double difficulty = 0;
- double weight = 1;
-
- foreach (double strain in peaks.OrderByDescending(d => d))
- {
- difficulty += strain * weight;
- weight *= 0.9;
- }
-
- return difficulty;
- }
-
- ///
- /// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars.
+ /// Applies a final re-scaling of the star rating.
///
/// The raw star rating value before re-scaling.
private double rescale(double sr)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs
index 68d0038b24..b61c13a2df 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceAttributes.cs
@@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
+ [JsonProperty("effective_miss_count")]
+ public double EffectiveMissCount { get; set; }
+
public override IEnumerable GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index 2c2dbddf13..95a1e8bc66 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countMeh;
private int countMiss;
+ private double effectiveMissCount;
+
public TaikoPerformanceCalculator()
: base(new TaikoRuleset())
{
@@ -35,7 +37,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
+ // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
+ if (totalSuccessfulHits > 0)
+ effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
+
+ double multiplier = 1.13;
if (score.Mods.Any(m => m is ModHidden))
multiplier *= 1.075;
@@ -55,6 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
Difficulty = difficultyValue,
Accuracy = accuracyValue,
+ EffectiveMissCount = effectiveMissCount,
Total = totalValue
};
}
@@ -66,18 +73,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus;
- difficultyValue *= Math.Pow(0.986, countMiss);
+ difficultyValue *= Math.Pow(0.986, effectiveMissCount);
if (score.Mods.Any(m => m is ModEasy))
- difficultyValue *= 0.980;
+ difficultyValue *= 0.985;
if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025;
- if (score.Mods.Any(m => m is ModFlashlight))
- difficultyValue *= 1.05 * lengthBonus;
+ if (score.Mods.Any(m => m is ModHardRock))
+ difficultyValue *= 1.050;
- return difficultyValue * Math.Pow(score.Accuracy, 1.5);
+ if (score.Mods.Any(m => m is ModFlashlight))
+ difficultyValue *= 1.050 * lengthBonus;
+
+ return difficultyValue * Math.Pow(score.Accuracy, 2.0);
}
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@@ -85,18 +95,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0)
return 0;
- double accuracyValue = Math.Pow(140.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 12.0) * 27;
+ double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;
- // Slight HDFL Bonus for accuracy.
+ // Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values
if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden))
- accuracyValue *= 1.10 * lengthBonus;
+ accuracyValue *= Math.Max(1.050, 1.075 * lengthBonus);
return accuracyValue;
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
+
+ private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
index e49043e58e..23a005190a 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
return;
base.OnMouseUp(e);
- EndPlacement(true);
+ EndPlacement(spanPlacementObject.Duration > 0);
}
public override void UpdateTimeAndPosition(SnapResult result)
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs
index ad6fdf59e2..009f2854f8 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -8,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModEasy : ModEasy
{
- public override string Description => @"Beats move slower, and less accuracy required!";
+ public override LocalisableString Description => @"Beats move slower, and less accuracy required!";
///
/// Multiplier factor added to the scrolling speed.
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
index 4c802978e3..4708ef9bf0 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset
{
- public override string Description => @"Beats fade out before you hit them!";
+ public override LocalisableString Description => @"Beats fade out before you hit them!";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
///
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs
index 307a37bf2e..c0be0290e6 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRandom.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModRandom : ModRandom, IApplicableToBeatmap
{
- public override string Description => @"Shuffle around the colours!";
+ public override LocalisableString Description => @"Shuffle around the colours!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(TaikoModSwap)).ToArray();
public void ApplyToBeatmap(IBeatmap beatmap)
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs
index 7be70d9ac3..d1e9ab1428 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs
@@ -1,12 +1,13 @@
// 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.Localisation;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModRelax : ModRelax
{
- public override string Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's.";
+ public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's.";
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs
index 3cb337c41d..fc3913f56d 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModSwap.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Beatmaps;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public override string Name => "Swap";
public override string Acronym => "SW";
- public override string Description => @"Dons become kats, kats become dons";
+ public override LocalisableString Description => @"Dons become kats, kats become dons";
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray();
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 04ed6d0b87..68e334332e 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
private const int rolling_hits_for_engaged_colour = 5;
+ public override Quad ScreenSpaceDrawQuad => MainPiece.Drawable.ScreenSpaceDrawQuad;
+
///
/// Rolling number of tick hits. This increases for hits and decreases for misses.
///
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
index 7f813e7b27..399bd9260d 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
@@ -20,6 +21,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
private Drawable backgroundLayer;
+ // required for editor blueprints (not sure why these circle pieces are zero size).
+ public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
+
public LegacyCirclePiece()
{
RelativeSizeAxes = Axes.Both;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs
index d3bf70e603..040d8ff965 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs
@@ -6,6 +6,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
@@ -16,11 +17,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class LegacyDrumRoll : CompositeDrawable, IHasAccentColour
{
+ public override Quad ScreenSpaceDrawQuad
+ {
+ get
+ {
+ var headDrawQuad = headCircle.ScreenSpaceDrawQuad;
+ var tailDrawQuad = tailCircle.ScreenSpaceDrawQuad;
+
+ return new Quad(headDrawQuad.TopLeft, tailDrawQuad.TopRight, headDrawQuad.BottomLeft, tailDrawQuad.BottomRight);
+ }
+ }
+
private LegacyCirclePiece headCircle;
private Sprite body;
- private Sprite end;
+ private Sprite tailCircle;
public LegacyDrumRoll()
{
@@ -32,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
InternalChildren = new Drawable[]
{
- end = new Sprite
+ tailCircle = new Sprite
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
@@ -82,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
headCircle.AccentColour = colour;
body.Colour = colour;
- end.Colour = colour;
+ tailCircle.Colour = colour;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 223e268d7f..04bb08395b 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -25,6 +25,7 @@ using osu.Game.Scoring;
using System;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit;
using osu.Game.Rulesets.Taiko.Objects;
@@ -192,7 +193,7 @@ namespace osu.Game.Rulesets.Taiko
};
}
- public override string GetDisplayNameForHitResult(HitResult result)
+ public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs
index 3b391f6756..aa41fd830b 100644
--- a/osu.Game.Tests/Mods/ModUtilsTest.cs
+++ b/osu.Game.Tests/Mods/ModUtilsTest.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using Moq;
using NUnit.Framework;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Utils;
@@ -320,7 +321,7 @@ namespace osu.Game.Tests.Mods
public class InvalidMultiplayerMod : Mod
{
public override string Name => string.Empty;
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override bool HasImplementation => true;
@@ -331,7 +332,7 @@ namespace osu.Game.Tests.Mods
private class InvalidMultiplayerFreeMod : Mod
{
public override string Name => string.Empty;
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override bool HasImplementation => true;
diff --git a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
index 9e3354935a..7df5448ff7 100644
--- a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
+++ b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
@@ -60,7 +61,7 @@ namespace osu.Game.Tests.Mods
{
public override double ScoreMultiplier => 1.0;
- public override string Description => "This is a customisable test mod.";
+ public override LocalisableString Description => "This is a customisable test mod.";
public override ModType Type => ModType.Conversion;
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index 8d1c266473..6637d640b2 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
@@ -160,7 +161,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => nameof(ModA);
public override string Acronym => nameof(ModA);
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) };
@@ -169,7 +170,7 @@ namespace osu.Game.Tests.NonVisual
private class ModB : Mod
{
public override string Name => nameof(ModB);
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public override string Acronym => nameof(ModB);
public override double ScoreMultiplier => 1;
@@ -180,7 +181,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => nameof(ModC);
public override string Acronym => nameof(ModC);
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public override double ScoreMultiplier => 1;
}
@@ -188,7 +189,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => $"Incompatible With {nameof(ModA)}";
public override string Acronym => $"Incompatible With {nameof(ModA)}";
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModA) };
@@ -207,7 +208,7 @@ namespace osu.Game.Tests.NonVisual
{
public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
public override string Acronym => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };
diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs
similarity index 59%
rename from osu.Game.Tests/NonVisual/GameplayClockTest.cs
rename to osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs
index 162734f9da..f9f4ead644 100644
--- a/osu.Game.Tests/NonVisual/GameplayClockTest.cs
+++ b/osu.Game.Tests/NonVisual/GameplayClockContainerTest.cs
@@ -1,39 +1,31 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
-using System.Linq;
using NUnit.Framework;
-using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
- public class GameplayClockTest
+ public class GameplayClockContainerTest
{
[TestCase(0)]
[TestCase(1)]
public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate)
{
var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
- var gameplayClock = new TestGameplayClock(framedClock);
-
- gameplayClock.MutableNonGameplayAdjustments.Add(new BindableDouble());
+ var gameplayClock = new TestGameplayClockContainer(framedClock);
Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0));
}
- private class TestGameplayClock : GameplayClock
+ private class TestGameplayClockContainer : GameplayClockContainer
{
- public List> MutableNonGameplayAdjustments { get; } = new List>();
+ public override IEnumerable NonGameplayAdjustments => new[] { 0.0 };
- public override IEnumerable NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value);
-
- public TestGameplayClock(IFrameBasedClock underlyingClock)
+ public TestGameplayClockContainer(IFrameBasedClock underlyingClock)
: base(underlyingClock)
{
}
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index 7458508c7a..17709fb10f 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -9,6 +9,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
@@ -182,7 +183,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
- public override string Description => "This is a test mod.";
+ public override LocalisableString Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
@@ -199,7 +200,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
- public override string Description => "This is a test mod.";
+ public override LocalisableString Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
index a89d68bf15..b17414e026 100644
--- a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using MessagePack;
using NUnit.Framework;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
@@ -102,7 +103,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
- public override string Description => "This is a test mod.";
+ public override LocalisableString Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
@@ -119,7 +120,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
- public override string Description => "This is a test mod.";
+ public override LocalisableString Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
@@ -154,7 +155,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
- public override string Description => "This is a test mod.";
+ public override LocalisableString Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
index 6c639ee539..6015c92663 100644
--- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
+++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
@@ -4,11 +4,13 @@
#nullable disable
using System;
+using System.Collections.Generic;
using NUnit.Framework;
-using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
+using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.OnlinePlay
@@ -16,20 +18,34 @@ namespace osu.Game.Tests.OnlinePlay
[HeadlessTest]
public class TestSceneCatchUpSyncManager : OsuTestScene
{
- private TestManualClock master;
- private CatchUpSyncManager syncManager;
+ private GameplayClockContainer master;
+ private SpectatorSyncManager syncManager;
- private TestSpectatorPlayerClock player1;
- private TestSpectatorPlayerClock player2;
+ private Dictionary clocksById;
+ private SpectatorPlayerClock player1;
+ private SpectatorPlayerClock player2;
[SetUp]
public void Setup()
{
- syncManager = new CatchUpSyncManager(master = new TestManualClock());
- syncManager.AddPlayerClock(player1 = new TestSpectatorPlayerClock(1));
- syncManager.AddPlayerClock(player2 = new TestSpectatorPlayerClock(2));
+ syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock()));
+ player1 = syncManager.CreateManagedClock();
+ player2 = syncManager.CreateManagedClock();
- Schedule(() => Child = syncManager);
+ clocksById = new Dictionary
+ {
+ { player1, 1 },
+ { player2, 2 }
+ };
+
+ Schedule(() =>
+ {
+ Children = new Drawable[]
+ {
+ syncManager,
+ master
+ };
+ });
}
[Test]
@@ -48,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay
public void TestReadyPlayersStartWhenReadyForMaximumDelayTime()
{
setWaiting(() => player1, false);
- AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
+ AddWaitStep($"wait {SpectatorSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction));
assertPlayerClockState(() => player1, true);
assertPlayerClockState(() => player2, false);
}
@@ -58,7 +74,7 @@ namespace osu.Game.Tests.OnlinePlay
{
setAllWaiting(false);
- setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1);
+ setMasterTime(SpectatorSyncManager.SYNC_TARGET + 1);
assertCatchingUp(() => player1, false);
}
@@ -67,7 +83,7 @@ namespace osu.Game.Tests.OnlinePlay
{
setAllWaiting(false);
- setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1);
+ setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1);
assertCatchingUp(() => player1, true);
assertCatchingUp(() => player2, true);
}
@@ -77,8 +93,8 @@ namespace osu.Game.Tests.OnlinePlay
{
setAllWaiting(false);
- setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1);
- setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET + 1);
+ setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1);
+ setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET + 1);
assertCatchingUp(() => player1, true);
}
@@ -87,8 +103,8 @@ namespace osu.Game.Tests.OnlinePlay
{
setAllWaiting(false);
- setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2);
- setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET);
+ setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 2);
+ setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET);
assertCatchingUp(() => player1, false);
assertCatchingUp(() => player2, true);
}
@@ -98,7 +114,7 @@ namespace osu.Game.Tests.OnlinePlay
{
setAllWaiting(false);
- setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET);
+ setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET);
assertCatchingUp(() => player1, false);
assertPlayerClockState(() => player1, true);
}
@@ -108,7 +124,7 @@ namespace osu.Game.Tests.OnlinePlay
{
setAllWaiting(false);
- setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET - 1);
+ setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET - 1);
// This is a silent catchup, where IsCatchingUp = false but IsRunning = false also.
assertCatchingUp(() => player1, false);
@@ -129,13 +145,13 @@ namespace osu.Game.Tests.OnlinePlay
assertPlayerClockState(() => player1, false);
}
- private void setWaiting(Func playerClock, bool waiting)
- => AddStep($"set player clock {playerClock().Id} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting);
+ private void setWaiting(Func playerClock, bool waiting)
+ => AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames = waiting);
private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () =>
{
- player1.WaitingOnFrames.Value = waiting;
- player2.WaitingOnFrames.Value = waiting;
+ player1.WaitingOnFrames = waiting;
+ player2.WaitingOnFrames = waiting;
});
private void setMasterTime(double time)
@@ -144,51 +160,14 @@ namespace osu.Game.Tests.OnlinePlay
///
/// clock.Time = master.Time - offsetFromMaster
///
- private void setPlayerClockTime(Func playerClock, double offsetFromMaster)
- => AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
+ private void setPlayerClockTime(Func playerClock, double offsetFromMaster)
+ => AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
- private void assertCatchingUp(Func playerClock, bool catchingUp) =>
- AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
+ private void assertCatchingUp(Func playerClock, bool catchingUp) =>
+ AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
- private void assertPlayerClockState(Func playerClock, bool running)
- => AddAssert($"player clock {playerClock().Id} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
-
- private class TestSpectatorPlayerClock : TestManualClock, ISpectatorPlayerClock
- {
- public Bindable WaitingOnFrames { get; } = new Bindable(true);
-
- public bool IsCatchingUp { get; set; }
-
- public IFrameBasedClock Source
- {
- set => throw new NotImplementedException();
- }
-
- public readonly int Id;
-
- public TestSpectatorPlayerClock(int id)
- {
- Id = id;
-
- WaitingOnFrames.BindValueChanged(waiting =>
- {
- if (waiting.NewValue)
- Stop();
- else
- Start();
- });
- }
-
- public void ProcessFrame()
- {
- }
-
- public double ElapsedFrameTime => 0;
-
- public double FramesPerSecond => 0;
-
- public FrameTimeInfo TimeInfo => default;
- }
+ private void assertPlayerClockState(Func playerClock, bool running)
+ => AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
private class TestManualClock : ManualClock, IAdjustableClock
{
diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
index ff282fff62..44ebdad2e4 100644
--- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
+++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -314,15 +315,55 @@ namespace osu.Game.Tests.Rulesets.Scoring
}), Is.EqualTo(expectedScore).Within(0.5d));
}
+#pragma warning disable CS0618
+ [Test]
+ public void TestLegacyComboIncrease()
+ {
+ Assert.That(HitResult.LegacyComboIncrease.IncreasesCombo(), Is.True);
+ Assert.That(HitResult.LegacyComboIncrease.BreaksCombo(), Is.False);
+ Assert.That(HitResult.LegacyComboIncrease.AffectsCombo(), Is.True);
+ Assert.That(HitResult.LegacyComboIncrease.AffectsAccuracy(), Is.False);
+ Assert.That(HitResult.LegacyComboIncrease.IsBasic(), Is.False);
+ Assert.That(HitResult.LegacyComboIncrease.IsTick(), Is.False);
+ Assert.That(HitResult.LegacyComboIncrease.IsBonus(), Is.False);
+ Assert.That(HitResult.LegacyComboIncrease.IsHit(), Is.True);
+ Assert.That(HitResult.LegacyComboIncrease.IsScorable(), Is.True);
+ Assert.That(HitResultExtensions.ALL_TYPES, Does.Not.Contain(HitResult.LegacyComboIncrease));
+
+ // Cannot be used to apply results.
+ Assert.Throws(() => scoreProcessor.ApplyBeatmap(new Beatmap
+ {
+ HitObjects = { new TestHitObject(HitResult.LegacyComboIncrease) }
+ }));
+
+ ScoreInfo testScore = new ScoreInfo
+ {
+ MaxCombo = 1,
+ Statistics = new Dictionary
+ {
+ { HitResult.Great, 1 }
+ },
+ MaximumStatistics = new Dictionary
+ {
+ { HitResult.Great, 1 },
+ { HitResult.LegacyComboIncrease, 1 }
+ }
+ };
+
+ double totalScore = new TestScoreProcessor().ComputeFinalScore(ScoringMode.Standardised, testScore);
+ Assert.That(totalScore, Is.EqualTo(750_000)); // 500K from accuracy (100%), and 250K from combo (50%).
+ }
+#pragma warning restore CS0618
+
private class TestRuleset : Ruleset
{
- public override IEnumerable GetModsFor(ModType type) => throw new System.NotImplementedException();
+ public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException();
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException();
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException();
- public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException();
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
- public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException();
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
public override string Description => string.Empty;
public override string ShortName => string.Empty;
@@ -352,5 +393,33 @@ namespace osu.Game.Tests.Rulesets.Scoring
this.maxResult = maxResult;
}
}
+
+ private class TestScoreProcessor : ScoreProcessor
+ {
+ protected override double DefaultAccuracyPortion => 0.5;
+ protected override double DefaultComboPortion => 0.5;
+
+ public TestScoreProcessor()
+ : base(new TestRuleset())
+ {
+ }
+
+ // ReSharper disable once MemberHidesStaticFromOuterClass
+ private class TestRuleset : Ruleset
+ {
+ protected override IEnumerable GetValidHitResults() => new[] { HitResult.Great };
+
+ public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException();
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException();
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
+
+ public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
+
+ public override string Description => string.Empty;
+ public override string ShortName => string.Empty;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
index d24baa6f63..2465512dae 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
@@ -173,6 +173,7 @@ namespace osu.Game.Tests.Visual.Editing
reset();
AddStep("Seek(49)", () => Clock.Seek(49));
+ checkTime(49);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
checkTime(50);
AddStep("Seek(49.999)", () => Clock.Seek(49.999));
@@ -207,6 +208,7 @@ namespace osu.Game.Tests.Visual.Editing
reset();
AddStep("Seek(450)", () => Clock.Seek(450));
+ checkTime(450);
AddStep("SeekBackward", () => Clock.SeekBackward());
checkTime(400);
AddStep("SeekBackward", () => Clock.SeekBackward());
@@ -228,6 +230,7 @@ namespace osu.Game.Tests.Visual.Editing
reset();
AddStep("Seek(450)", () => Clock.Seek(450));
+ checkTime(450);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
checkTime(400);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
@@ -252,6 +255,7 @@ namespace osu.Game.Tests.Visual.Editing
reset();
AddStep("Seek(451)", () => Clock.Seek(451));
+ checkTime(451);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
checkTime(450);
AddStep("Seek(450.999)", () => Clock.Seek(450.999));
@@ -276,6 +280,7 @@ namespace osu.Game.Tests.Visual.Editing
double lastTime = 0;
AddStep("Seek(0)", () => Clock.Seek(0));
+ checkTime(0);
for (int i = 0; i < 9; i++)
{
diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
index 78f650f0fa..674476d644 100644
--- a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
@@ -1,13 +1,9 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Beatmaps;
-using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osuTK;
@@ -19,19 +15,12 @@ namespace osu.Game.Tests.Visual.Editing
[BackgroundDependencyLoader]
private void load()
{
- var clock = new EditorClock { IsCoupled = false };
- Dependencies.CacheAs(clock);
-
- var playback = new PlaybackControl
+ Child = new PlaybackControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 100)
};
-
- Beatmap.Value = CreateWorkingBeatmap(new Beatmap());
-
- Child = playback;
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index 3e2698bc05..da6604a653 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached(typeof(IGameplayClock))]
- private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock());
+ private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
// best way to check without exposing.
private Drawable hideTarget => hudOverlay.KeyCounter;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
index 0d80d29cab..25251bf1d6 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.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 NUnit.Framework;
using osu.Framework.Graphics;
@@ -19,7 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneLeadIn : RateAdjustedBeatmapTestScene
{
- private LeadInPlayer player;
+ private LeadInPlayer player = null!;
private const double lenience_ms = 10;
@@ -36,11 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
BeatmapInfo = { AudioLeadIn = leadIn }
});
- AddStep("check first frame time", () =>
- {
- Assert.That(player.FirstFrameClockTime, Is.Not.Null);
- Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
- });
+ checkFirstFrameTime(expectedStartTime);
}
[TestCase(1000, 0)]
@@ -59,11 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
- AddStep("check first frame time", () =>
- {
- Assert.That(player.FirstFrameClockTime, Is.Not.Null);
- Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
- });
+ checkFirstFrameTime(expectedStartTime);
}
[TestCase(1000, 0, false)]
@@ -97,14 +87,13 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
- AddStep("check first frame time", () =>
- {
- Assert.That(player.FirstFrameClockTime, Is.Not.Null);
- Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
- });
+ checkFirstFrameTime(expectedStartTime);
}
- private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
+ private void checkFirstFrameTime(double expectedStartTime) =>
+ AddAssert("check first frame time", () => player.FirstFrameClockTime, () => Is.EqualTo(expectedStartTime).Within(lenience_ms));
+
+ private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
{
AddStep("create player", () =>
{
@@ -126,12 +115,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
- public double GameplayStartTime => DrawableRuleset.GameplayStartTime;
-
public double FirstHitObjectTime => DrawableRuleset.Objects.First().StartTime;
- public double GameplayClockTime => GameplayClockContainer.CurrentTime;
-
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index b0d7eadaa7..922529cf19 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -13,6 +13,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -459,7 +460,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override string Name => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public bool Applied { get; private set; }
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
index e29101ba8d..6c02ddab14 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached(typeof(IGameplayClock))]
- private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock());
+ private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
[SetUpSteps]
public void SetUpSteps()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
index 00e4171eac..485c76ac5c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[Cached(typeof(IGameplayClock))]
- private readonly IGameplayClock gameplayClock = new GameplayClock(new FramedClock());
+ private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
private IEnumerable hudOverlays => CreatedDrawables.OfType();
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index a2e3ab7318..bab613bed7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
checkPausedInstant(PLAYER_2_ID, true);
// Wait for the start delay seconds...
- AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
+ AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction));
// Player 1 should start playing by itself, player 2 should remain paused.
checkPausedInstant(PLAYER_1_ID, false);
@@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
loadSpectateScreen();
sendFrames(PLAYER_1_ID, 300);
- AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
+ AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction));
checkPaused(PLAYER_1_ID, false);
sendFrames(PLAYER_2_ID, 300);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
index 98de712703..dc2a687bd5 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osuTK;
@@ -39,6 +40,9 @@ namespace osu.Game.Tests.Visual.Settings
[SettingSource("Sample bool", "Clicking this changes a setting")]
public BindableBool TickBindable { get; } = new BindableBool();
+ [SettingSource(typeof(TestStrings), nameof(TestStrings.LocalisableLabel), nameof(TestStrings.LocalisableDescription))]
+ public BindableBool LocalisableBindable { get; } = new BindableBool(true);
+
[SettingSource("Sample float", "Change something for a mod")]
public BindableFloat SliderBindable { get; } = new BindableFloat
{
@@ -75,5 +79,11 @@ namespace osu.Game.Tests.Visual.Settings
Value1,
Value2
}
+
+ private class TestStrings
+ {
+ public static LocalisableString LocalisableLabel => new LocalisableString("Sample localisable label");
+ public static LocalisableString LocalisableDescription => new LocalisableString("Sample localisable description");
+ }
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index bb9e83a21c..c3e485d56b 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -244,8 +244,12 @@ namespace osu.Game.Tests.Visual.SongSelect
const int total_set_count = 200;
- for (int i = 0; i < total_set_count; i++)
- sets.Add(TestResources.CreateTestBeatmapSetInfo());
+ AddStep("Populuate beatmap sets", () =>
+ {
+ sets.Clear();
+ for (int i = 0; i < total_set_count; i++)
+ sets.Add(TestResources.CreateTestBeatmapSetInfo());
+ });
loadBeatmaps(sets);
@@ -275,8 +279,12 @@ namespace osu.Game.Tests.Visual.SongSelect
const int total_set_count = 20;
- for (int i = 0; i < total_set_count; i++)
- sets.Add(TestResources.CreateTestBeatmapSetInfo(3));
+ AddStep("Populuate beatmap sets", () =>
+ {
+ sets.Clear();
+ for (int i = 0; i < total_set_count; i++)
+ sets.Add(TestResources.CreateTestBeatmapSetInfo(3));
+ });
loadBeatmaps(sets);
@@ -493,18 +501,23 @@ namespace osu.Game.Tests.Visual.SongSelect
const string zzz_string = "zzzzz";
- for (int i = 0; i < 20; i++)
+ AddStep("Populuate beatmap sets", () =>
{
- var set = TestResources.CreateTestBeatmapSetInfo();
+ sets.Clear();
- if (i == 4)
- set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
+ for (int i = 0; i < 20; i++)
+ {
+ var set = TestResources.CreateTestBeatmapSetInfo();
- if (i == 16)
- set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string);
+ if (i == 4)
+ set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
- sets.Add(set);
- }
+ if (i == 16)
+ set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string);
+
+ sets.Add(set);
+ }
+ });
loadBeatmaps(sets);
@@ -521,21 +534,27 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestSortingStability()
{
var sets = new List();
+ int idOffset = 0;
- for (int i = 0; i < 10; i++)
+ AddStep("Populuate beatmap sets", () =>
{
- var set = TestResources.CreateTestBeatmapSetInfo();
+ sets.Clear();
- // only need to set the first as they are a shared reference.
- var beatmap = set.Beatmaps.First();
+ for (int i = 0; i < 10; i++)
+ {
+ var set = TestResources.CreateTestBeatmapSetInfo();
- beatmap.Metadata.Artist = $"artist {i / 2}";
- beatmap.Metadata.Title = $"title {9 - i}";
+ // only need to set the first as they are a shared reference.
+ var beatmap = set.Beatmaps.First();
- sets.Add(set);
- }
+ beatmap.Metadata.Artist = $"artist {i / 2}";
+ beatmap.Metadata.Title = $"title {9 - i}";
- int idOffset = sets.First().OnlineID;
+ sets.Add(set);
+ }
+
+ idOffset = sets.First().OnlineID;
+ });
loadBeatmaps(sets);
@@ -556,26 +575,32 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestSortingStabilityWithNewItems()
{
List sets = new List();
+ int idOffset = 0;
- for (int i = 0; i < 3; i++)
+ AddStep("Populuate beatmap sets", () =>
{
- var set = TestResources.CreateTestBeatmapSetInfo(3);
+ sets.Clear();
- // only need to set the first as they are a shared reference.
- var beatmap = set.Beatmaps.First();
+ for (int i = 0; i < 3; i++)
+ {
+ var set = TestResources.CreateTestBeatmapSetInfo(3);
- beatmap.Metadata.Artist = "same artist";
- beatmap.Metadata.Title = "same title";
+ // only need to set the first as they are a shared reference.
+ var beatmap = set.Beatmaps.First();
- sets.Add(set);
- }
+ beatmap.Metadata.Artist = "same artist";
+ beatmap.Metadata.Title = "same title";
- int idOffset = sets.First().OnlineID;
+ sets.Add(set);
+ }
+
+ idOffset = sets.First().OnlineID;
+ });
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
- AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
+ assertOriginalOrderMaintained();
AddStep("Add new item", () =>
{
@@ -590,10 +615,16 @@ namespace osu.Game.Tests.Visual.SongSelect
carousel.UpdateBeatmapSet(set);
});
- AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
+ assertOriginalOrderMaintained();
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
- AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.OnlineID == idOffset + index).All(b => b));
+ assertOriginalOrderMaintained();
+
+ void assertOriginalOrderMaintained()
+ {
+ AddAssert("Items remain in original order",
+ () => carousel.BeatmapSets.Select(s => s.OnlineID), () => Is.EqualTo(carousel.BeatmapSets.Select((set, index) => idOffset + index)));
+ }
}
[Test]
@@ -601,13 +632,18 @@ namespace osu.Game.Tests.Visual.SongSelect
{
List sets = new List();
- for (int i = 0; i < 3; i++)
+ AddStep("Populuate beatmap sets", () =>
{
- var set = TestResources.CreateTestBeatmapSetInfo(3);
- set.Beatmaps[0].StarRating = 3 - i;
- set.Beatmaps[2].StarRating = 6 + i;
- sets.Add(set);
- }
+ sets.Clear();
+
+ for (int i = 0; i < 3; i++)
+ {
+ var set = TestResources.CreateTestBeatmapSetInfo(3);
+ set.Beatmaps[0].StarRating = 3 - i;
+ set.Beatmaps[2].StarRating = 6 + i;
+ sets.Add(set);
+ }
+ });
loadBeatmaps(sets);
@@ -759,8 +795,13 @@ namespace osu.Game.Tests.Visual.SongSelect
{
List manySets = new List();
- for (int i = 1; i <= 50; i++)
- manySets.Add(TestResources.CreateTestBeatmapSetInfo(3));
+ AddStep("Populuate beatmap sets", () =>
+ {
+ manySets.Clear();
+
+ for (int i = 1; i <= 50; i++)
+ manySets.Add(TestResources.CreateTestBeatmapSetInfo(3));
+ });
loadBeatmaps(manySets);
@@ -791,6 +832,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("populate maps", () =>
{
+ manySets.Clear();
+
for (int i = 0; i < 10; i++)
{
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3, new[]
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 07473aa55b..4c43a2fdcd 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -1,16 +1,16 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
@@ -29,10 +29,18 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestFixture]
public class TestSceneModSelectOverlay : OsuManualInputManagerTestScene
{
- [Resolved]
- private RulesetStore rulesetStore { get; set; }
+ protected override bool UseFreshStoragePerRun => true;
- private UserModSelectOverlay modSelectOverlay;
+ private RulesetStore rulesetStore = null!;
+
+ private TestModSelectOverlay modSelectOverlay = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
+ Dependencies.Cache(Realm);
+ }
[SetUpSteps]
public void SetUpSteps()
@@ -40,11 +48,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear contents", Clear);
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault());
+ AddStep("set up presets", () =>
+ {
+ Realm.Write(r =>
+ {
+ r.RemoveAll();
+ r.Add(new ModPreset
+ {
+ Name = "AR0",
+ Description = "Too... many... circles...",
+ Ruleset = r.Find(OsuRuleset.SHORT_NAME),
+ Mods = new[]
+ {
+ new OsuModDifficultyAdjust
+ {
+ ApproachRate = { Value = 0 }
+ }
+ }
+ });
+ });
+ });
}
private void createScreen()
{
- AddStep("create screen", () => Child = modSelectOverlay = new UserModSelectOverlay
+ AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
@@ -137,7 +165,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("any column dimmed", () => this.ChildrenOfType().Any(column => !column.Active.Value));
- ModSelectColumn lastColumn = null;
+ ModSelectColumn lastColumn = null!;
AddAssert("last column dimmed", () => !this.ChildrenOfType().Last().Active.Value);
AddStep("request scroll to last column", () =>
@@ -165,18 +193,22 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("select customisable mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
assertCustomisationToggleState(disabled: false, active: false);
- AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ AddStep("select mod requiring configuration externally", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ assertCustomisationToggleState(disabled: false, active: false);
+
+ AddStep("reset mods", () => SelectedMods.SetDefault());
+ AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via toggle", () =>
{
- InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single());
+ InputManager.MoveMouseTo(modSelectOverlay.CustomisationButton);
InputManager.Click(MouseButton.Left);
});
assertCustomisationToggleState(disabled: false, active: false);
AddStep("reset mods", () => SelectedMods.SetDefault());
- AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape));
@@ -188,11 +220,18 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
assertCustomisationToggleState(disabled: true, active: false);
- AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("select mod without configuration", () => SelectedMods.Value = new[] { new OsuModAutoplay() });
assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
+
+ AddStep("select mod preset with mod requiring configuration", () =>
+ {
+ InputManager.MoveMouseTo(this.ChildrenOfType().First());
+ InputManager.Click(MouseButton.Left);
+ });
+ assertCustomisationToggleState(disabled: false, active: false);
}
[Test]
@@ -201,7 +240,7 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
- AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
+ AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single()));
@@ -224,11 +263,11 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestSettingsNotCrossPolluting()
{
- Bindable> selectedMods2 = null;
- ModSelectOverlay modSelectOverlay2 = null;
+ Bindable> selectedMods2 = null!;
+ ModSelectOverlay modSelectOverlay2 = null!;
createScreen();
- AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
+ AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
AddStep("set setting", () => modSelectOverlay.ChildrenOfType>().First().Current.Value = 8);
@@ -353,7 +392,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestExternallySetModIsReplacedByOverlayInstance()
{
Mod external = new OsuModDoubleTime();
- Mod overlayButtonMod = null;
+ Mod overlayButtonMod = null!;
createScreen();
changeRuleset(0);
@@ -458,14 +497,14 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
changeRuleset(0);
- AddStep("select difficulty adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
+ AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
- AddAssert("back button disabled", () => !this.ChildrenOfType().First().Enabled.Value);
+ AddAssert("back button disabled", () => !modSelectOverlay.BackButton.Enabled.Value);
AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape));
AddStep("click back button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().First());
+ InputManager.MoveMouseTo(modSelectOverlay.BackButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
@@ -474,7 +513,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestColumnHiding()
{
- AddStep("create screen", () => Child = modSelectOverlay = new UserModSelectOverlay
+ AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
@@ -527,20 +566,26 @@ namespace osu.Game.Tests.Visual.UserInterface
private void assertCustomisationToggleState(bool disabled, bool active)
{
- ShearedToggleButton getToggle() => modSelectOverlay.ChildrenOfType().Single();
-
- AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => getToggle().Active.Disabled == disabled);
- AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => getToggle().Active.Value == active);
+ AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Disabled == disabled);
+ AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active);
}
private ModPanel getPanelForMod(Type modType)
=> modSelectOverlay.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType);
+ private class TestModSelectOverlay : UserModSelectOverlay
+ {
+ protected override bool ShowPresets => true;
+
+ public new ShearedButton BackButton => base.BackButton;
+ public new ShearedToggleButton? CustomisationButton => base.CustomisationButton;
+ }
+
private class TestUnimplementedMod : Mod
{
public override string Name => "Unimplemented mod";
public override string Acronym => "UM";
- public override string Description => "A mod that is not implemented.";
+ public override LocalisableString Description => "A mod that is not implemented.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.Conversion;
}
diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
index 21e67363c3..630b65ae82 100644
--- a/osu.Game/Configuration/SettingSourceAttribute.cs
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -9,6 +9,7 @@ using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@@ -43,12 +44,47 @@ namespace osu.Game.Configuration
///
public Type? SettingControlType { get; set; }
+ public SettingSourceAttribute(Type declaringType, string label, string? description = null)
+ {
+ Label = getLocalisableStringFromMember(label) ?? string.Empty;
+ Description = getLocalisableStringFromMember(description) ?? string.Empty;
+
+ LocalisableString? getLocalisableStringFromMember(string? member)
+ {
+ if (member == null)
+ return null;
+
+ var property = declaringType.GetMember(member, BindingFlags.Static | BindingFlags.Public).FirstOrDefault();
+
+ if (property == null)
+ return null;
+
+ switch (property)
+ {
+ case FieldInfo f:
+ return (LocalisableString)f.GetValue(null).AsNonNull();
+
+ case PropertyInfo p:
+ return (LocalisableString)p.GetValue(null).AsNonNull();
+
+ default:
+ throw new InvalidOperationException($"Member \"{member}\" was not found in type {declaringType} (must be a static field or property)");
+ }
+ }
+ }
+
public SettingSourceAttribute(string? label, string? description = null)
{
Label = label ?? string.Empty;
Description = description ?? string.Empty;
}
+ public SettingSourceAttribute(Type declaringType, string label, string description, int orderPosition)
+ : this(declaringType, label, description)
+ {
+ OrderPosition = orderPosition;
+ }
+
public SettingSourceAttribute(string label, string description, int orderPosition)
: this(label, description)
{
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 0f2e724567..e23fc912df 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -69,8 +69,9 @@ namespace osu.Game.Database
/// 21 2022-07-27 Migrate collections to realm (BeatmapCollection).
/// 22 2022-07-31 Added ModPreset.
/// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo.
+ /// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
///
- private const int schema_version = 23;
+ private const int schema_version = 24;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
diff --git a/osu.Game/Graphics/UserInterfaceV2/ColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/ColourPalette.cs
index 001ccc7f87..f61d6db8b1 100644
--- a/osu.Game/Graphics/UserInterfaceV2/ColourPalette.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/ColourPalette.cs
@@ -13,6 +13,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
@@ -26,9 +27,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
public BindableList Colours { get; } = new BindableList();
- private string colourNamePrefix = "Colour";
+ private LocalisableString colourNamePrefix = "Colour";
- public string ColourNamePrefix
+ public LocalisableString ColourNamePrefix
{
get => colourNamePrefix;
set
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs
index 7b9684e3ef..b144f8f696 100644
--- a/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledColourPalette.cs
@@ -5,6 +5,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -17,7 +18,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public BindableList Colours => Component.Colours;
- public string ColourNamePrefix
+ public LocalisableString ColourNamePrefix
{
get => Component.ColourNamePrefix;
set => Component.ColourNamePrefix = value;
diff --git a/osu.Game/Localisation/EditorSetupStrings.cs b/osu.Game/Localisation/EditorSetupStrings.cs
new file mode 100644
index 0000000000..0431b9cf76
--- /dev/null
+++ b/osu.Game/Localisation/EditorSetupStrings.cs
@@ -0,0 +1,204 @@
+// 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.Localisation;
+
+namespace osu.Game.Localisation
+{
+ public static class EditorSetupStrings
+ {
+ private const string prefix = @"osu.Game.Resources.Localisation.EditorSetup";
+
+ ///
+ /// "Beatmap Setup"
+ ///
+ public static LocalisableString BeatmapSetup => new TranslatableString(getKey(@"beatmap_setup"), @"Beatmap Setup");
+
+ ///
+ /// "change general settings of your beatmap"
+ ///
+ public static LocalisableString BeatmapSetupDescription => new TranslatableString(getKey(@"beatmap_setup_description"), @"change general settings of your beatmap");
+
+ ///
+ /// "Colours"
+ ///
+ public static LocalisableString ColoursHeader => new TranslatableString(getKey(@"colours_header"), @"Colours");
+
+ ///
+ /// "Hit circle / Slider Combos"
+ ///
+ public static LocalisableString HitCircleSliderCombos => new TranslatableString(getKey(@"hit_circle_slider_combos"), @"Hit circle / Slider Combos");
+
+ ///
+ /// "Design"
+ ///
+ public static LocalisableString DesignHeader => new TranslatableString(getKey(@"design_header"), @"Design");
+
+ ///
+ /// "Enable countdown"
+ ///
+ public static LocalisableString EnableCountdown => new TranslatableString(getKey(@"enable_countdown"), @"Enable countdown");
+
+ ///
+ /// "If enabled, an "Are you ready? 3, 2, 1, GO!" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."
+ ///
+ public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so.");
+
+ ///
+ /// "Countdown speed"
+ ///
+ public static LocalisableString CountdownSpeed => new TranslatableString(getKey(@"countdown_speed"), @"Countdown speed");
+
+ ///
+ /// "If the countdown sounds off-time, use this to make it appear one or more beats early."
+ ///
+ public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early.");
+
+ ///
+ /// "Countdown offset"
+ ///
+ public static LocalisableString CountdownOffset => new TranslatableString(getKey(@"countdown_offset"), @"Countdown offset");
+
+ ///
+ /// "Widescreen support"
+ ///
+ public static LocalisableString WidescreenSupport => new TranslatableString(getKey(@"widescreen_support"), @"Widescreen support");
+
+ ///
+ /// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."
+ ///
+ public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.");
+
+ ///
+ /// "Epilepsy warning"
+ ///
+ public static LocalisableString EpilepsyWarning => new TranslatableString(getKey(@"epilepsy_warning"), @"Epilepsy warning");
+
+ ///
+ /// "Recommended if the storyboard or video contain scenes with rapidly flashing colours."
+ ///
+ public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours.");
+
+ ///
+ /// "Letterbox during breaks"
+ ///
+ public static LocalisableString LetterboxDuringBreaks => new TranslatableString(getKey(@"letterbox_during_breaks"), @"Letterbox during breaks");
+
+ ///
+ /// "Adds horizontal letterboxing to give a cinematic look during breaks."
+ ///
+ public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks.");
+
+ ///
+ /// "Samples match playback rate"
+ ///
+ public static LocalisableString SamplesMatchPlaybackRate => new TranslatableString(getKey(@"samples_match_playback_rate"), @"Samples match playback rate");
+
+ ///
+ /// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled."
+ ///
+ public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled.");
+
+ ///
+ /// "The size of all hit objects"
+ ///
+ public static LocalisableString CircleSizeDescription => new TranslatableString(getKey(@"circle_size_description"), @"The size of all hit objects");
+
+ ///
+ /// "The rate of passive health drain throughout playable time"
+ ///
+ public static LocalisableString DrainRateDescription => new TranslatableString(getKey(@"drain_rate_description"), @"The rate of passive health drain throughout playable time");
+
+ ///
+ /// "The speed at which objects are presented to the player"
+ ///
+ public static LocalisableString ApproachRateDescription => new TranslatableString(getKey(@"approach_rate_description"), @"The speed at which objects are presented to the player");
+
+ ///
+ /// "The harshness of hit windows and difficulty of special objects (ie. spinners)"
+ ///
+ public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)");
+
+ ///
+ /// "Metadata"
+ ///
+ public static LocalisableString MetadataHeader => new TranslatableString(getKey(@"metadata_header"), @"Metadata");
+
+ ///
+ /// "Romanised Artist"
+ ///
+ public static LocalisableString RomanisedArtist => new TranslatableString(getKey(@"romanised_artist"), @"Romanised Artist");
+
+ ///
+ /// "Romanised Title"
+ ///
+ public static LocalisableString RomanisedTitle => new TranslatableString(getKey(@"romanised_title"), @"Romanised Title");
+
+ ///
+ /// "Creator"
+ ///
+ public static LocalisableString Creator => new TranslatableString(getKey(@"creator"), @"Creator");
+
+ ///
+ /// "Difficulty Name"
+ ///
+ public static LocalisableString DifficultyName => new TranslatableString(getKey(@"difficulty_name"), @"Difficulty Name");
+
+ ///
+ /// "Resources"
+ ///
+ public static LocalisableString ResourcesHeader => new TranslatableString(getKey(@"resources_header"), @"Resources");
+
+ ///
+ /// "Audio Track"
+ ///
+ public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track");
+
+ ///
+ /// "Click to select a track"
+ ///
+ public static LocalisableString ClickToSelectTrack => new TranslatableString(getKey(@"click_to_select_track"), @"Click to select a track");
+
+ ///
+ /// "Click to replace the track"
+ ///
+ public static LocalisableString ClickToReplaceTrack => new TranslatableString(getKey(@"click_to_replace_track"), @"Click to replace the track");
+
+ ///
+ /// "Click to select a background image"
+ ///
+ public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image");
+
+ ///
+ /// "Click to replace the background image"
+ ///
+ public static LocalisableString ClickToReplaceBackground => new TranslatableString(getKey(@"click_to_replace_background"), @"Click to replace the background image");
+
+ ///
+ /// "Ruleset ({0})"
+ ///
+ public static LocalisableString RulesetHeader(string arg0) => new TranslatableString(getKey(@"ruleset"), @"Ruleset ({0})", arg0);
+
+ ///
+ /// "Combo"
+ ///
+ public static LocalisableString ComboColourPrefix => new TranslatableString(getKey(@"combo_colour_prefix"), @"Combo");
+
+ ///
+ /// "Artist"
+ ///
+ public static LocalisableString Artist => new TranslatableString(getKey(@"artist"), @"Artist");
+
+ ///
+ /// "Title"
+ ///
+ public static LocalisableString Title => new TranslatableString(getKey(@"title"), @"Title");
+
+ ///
+ /// "Difficulty"
+ ///
+ public static LocalisableString DifficultyHeader => new TranslatableString(getKey(@"difficulty_header"), @"Difficulty");
+
+ private static string getKey(string key) => $@"{prefix}:{key}";
+ }
+}
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 07d544260e..7dc34d1293 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -128,5 +128,13 @@ namespace osu.Game.Online.API
IBindable IAPIProvider.Activity => Activity;
public void FailNextLogin() => shouldFailNextLogin = true;
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ // Ensure (as much as we can) that any pending tasks are run.
+ Scheduler.Update();
+ }
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
index e2e5ea4239..16aa800cb0 100644
--- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
@@ -74,6 +74,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("statistics")]
public Dictionary Statistics { get; set; } = new Dictionary();
+ [JsonProperty("maximum_statistics")]
+ public Dictionary MaximumStatistics { get; set; } = new Dictionary();
+
#region osu-web API additions (not stored to database).
[JsonProperty("id")]
@@ -153,6 +156,7 @@ namespace osu.Game.Online.API.Requests.Responses
MaxCombo = MaxCombo,
Rank = Rank,
Statistics = Statistics,
+ MaximumStatistics = MaximumStatistics,
Date = EndedAt,
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
Mods = mods,
@@ -174,6 +178,7 @@ namespace osu.Game.Online.API.Requests.Responses
Passed = score.Passed,
Mods = score.APIMods,
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
+ MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
};
public long OnlineID => ID ?? -1;
diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs
index e557b9933e..587159179f 100644
--- a/osu.Game/Online/Chat/ExternalLinkOpener.cs
+++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs
@@ -1,27 +1,27 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Game.Configuration;
using osu.Game.Overlays;
-using osu.Game.Overlays.Chat;
+using osu.Game.Overlays.Dialog;
namespace osu.Game.Online.Chat
{
public class ExternalLinkOpener : Component
{
[Resolved]
- private GameHost host { get; set; }
+ private GameHost host { get; set; } = null!;
[Resolved(CanBeNull = true)]
- private IDialogOverlay dialogOverlay { get; set; }
+ private IDialogOverlay? dialogOverlay { get; set; }
- private Bindable externalLinkWarning;
+ private Bindable externalLinkWarning = null!;
[BackgroundDependencyLoader(true)]
private void load(OsuConfigManager config)
@@ -31,10 +31,39 @@ namespace osu.Game.Online.Chat
public void OpenUrlExternally(string url, bool bypassWarning = false)
{
- if (!bypassWarning && externalLinkWarning.Value)
- dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url)));
+ if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null)
+ dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => host.GetClipboard()?.SetText(url)));
else
host.OpenUrlExternally(url);
}
+
+ public class ExternalLinkDialog : PopupDialog
+ {
+ public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction)
+ {
+ HeaderText = "Just checking...";
+ BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}";
+
+ Icon = FontAwesome.Solid.ExclamationTriangle;
+
+ Buttons = new PopupDialogButton[]
+ {
+ new PopupDialogOkButton
+ {
+ Text = @"Yes. Go for it.",
+ Action = openExternalLinkAction
+ },
+ new PopupDialogCancelButton
+ {
+ Text = @"Copy URL to the clipboard instead.",
+ Action = copyExternalLinkAction
+ },
+ new PopupDialogCancelButton
+ {
+ Text = @"No! Abort mission!"
+ },
+ };
+ }
+ }
}
}
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
index 53a4719050..2f3ece0e3b 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
@@ -10,6 +10,8 @@ using osuTK;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
@@ -126,7 +128,7 @@ namespace osu.Game.Online.Leaderboards
private class HitResultCell : CompositeDrawable
{
- private readonly string displayName;
+ private readonly LocalisableString displayName;
private readonly HitResult result;
private readonly int count;
@@ -134,7 +136,7 @@ namespace osu.Game.Online.Leaderboards
{
AutoSizeAxes = Axes.Both;
- displayName = stat.DisplayName;
+ displayName = stat.DisplayName.ToUpper();
result = stat.Result;
count = stat.Count;
}
@@ -153,7 +155,7 @@ namespace osu.Game.Online.Leaderboards
new OsuSpriteText
{
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
- Text = displayName.ToUpperInvariant(),
+ Text = displayName.ToUpper(),
Colour = colours.ForHitResult(result),
},
new OsuSpriteText
diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs
index d54b8ca75d..fcea650e2d 100644
--- a/osu.Game/Online/PollingComponent.cs
+++ b/osu.Game/Online/PollingComponent.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Online
///
/// A component which requires a constant polling process.
///
- public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages.
+ public abstract class PollingComponent : CompositeComponent
{
private double? lastTimePolled;
diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
index 2fd8445980..bb8ec4f6ff 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
@@ -27,13 +27,10 @@ namespace osu.Game.Online.Rooms
/// This differs from a regular download tracking composite as this accounts for the
/// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap.
///
- public class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable
+ public class OnlinePlayBeatmapAvailabilityTracker : CompositeComponent
{
public readonly IBindable SelectedItem = new Bindable();
- // Required to allow child components to update. Can potentially be replaced with a `CompositeComponent` class if or when we make one.
- protected override bool RequiresChildrenUpdate => true;
-
[Resolved]
private RealmAccess realm { get; set; } = null!;
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 5463c7a50f..11aefd435d 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
///
/// The statistics that appear in the table, in order of appearance.
///
- private readonly List<(HitResult result, string displayName)> statisticResultTypes = new List<(HitResult, string)>();
+ private readonly List<(HitResult result, LocalisableString displayName)> statisticResultTypes = new List<(HitResult, LocalisableString)>();
private bool showPerformancePoints;
@@ -114,7 +114,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
if (result.IsBonus())
continue;
- string displayName = ruleset.GetDisplayNameForHitResult(result);
+ var displayName = ruleset.GetDisplayNameForHitResult(result);
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
statisticResultTypes.Add((result, displayName));
diff --git a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs b/osu.Game/Overlays/Chat/ExternalLinkDialog.cs
deleted file mode 100644
index f0d39346e0..0000000000
--- a/osu.Game/Overlays/Chat/ExternalLinkDialog.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Overlays.Dialog;
-
-namespace osu.Game.Overlays.Chat
-{
- public class ExternalLinkDialog : PopupDialog
- {
- public ExternalLinkDialog(string url, Action openExternalLinkAction)
- {
- HeaderText = "Just checking...";
- BodyText = $"You are about to leave osu! and open the following link in a web browser:\n\n{url}";
-
- Icon = FontAwesome.Solid.ExclamationTriangle;
-
- Buttons = new PopupDialogButton[]
- {
- new PopupDialogOkButton
- {
- Text = @"Yes. Go for it.",
- Action = openExternalLinkAction
- },
- new PopupDialogCancelButton
- {
- Text = @"No! Abort mission!"
- },
- };
- }
- }
-}
diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
index 255d01466f..835883fb93 100644
--- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
+++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
@@ -31,10 +31,7 @@ namespace osu.Game.Overlays.Mods
set => current.Current = value;
}
- private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(1)
- {
- Precision = 0.01
- };
+ private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(1);
private readonly Box underlayBackground;
private readonly Box contentBackground;
diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs
index 7a2c727a00..b9f7114f74 100644
--- a/osu.Game/Overlays/Mods/ModColumn.cs
+++ b/osu.Game/Overlays/Mods/ModColumn.cs
@@ -66,7 +66,10 @@ namespace osu.Game.Overlays.Mods
private IModHotkeyHandler hotkeyHandler = null!;
private Task? latestLoadTask;
- internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true;
+ private ICollection? latestLoadedPanels;
+ internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && latestLoadedPanels?.All(panel => panel.Parent != null) == true;
+
+ public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
public ModColumn(ModType modType, bool allowIncompatibleSelection)
{
@@ -130,7 +133,8 @@ namespace osu.Game.Overlays.Mods
{
cancellationTokenSource?.Cancel();
- var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero));
+ var panels = availableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = Vector2.Zero)).ToArray();
+ latestLoadedPanels = panels;
latestLoadTask = LoadComponentsAsync(panels, loaded =>
{
diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs
index 6ef6ab0595..7bdb9511ac 100644
--- a/osu.Game/Overlays/Mods/ModPanel.cs
+++ b/osu.Game/Overlays/Mods/ModPanel.cs
@@ -57,6 +57,18 @@ namespace osu.Game.Overlays.Mods
Filtered.BindValueChanged(_ => updateFilterState(), true);
}
+ protected override void Select()
+ {
+ modState.PendingConfiguration = Mod.RequiresConfiguration;
+ Active.Value = true;
+ }
+
+ protected override void Deselect()
+ {
+ modState.PendingConfiguration = false;
+ Active.Value = false;
+ }
+
#region Filtering support
private void updateFilterState()
diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs
index a259645479..b314a19142 100644
--- a/osu.Game/Overlays/Mods/ModPresetPanel.cs
+++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs
@@ -37,8 +37,6 @@ namespace osu.Game.Overlays.Mods
Title = preset.Value.Name;
Description = preset.Value.Description;
-
- Action = toggleRequestedByUser;
}
[BackgroundDependencyLoader]
@@ -54,15 +52,19 @@ namespace osu.Game.Overlays.Mods
selectedMods.BindValueChanged(_ => selectedModsChanged(), true);
}
- private void toggleRequestedByUser()
+ protected override void Select()
+ {
+ // if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections,
+ // which will also have the side effect of activating the preset (see `updateActiveState()`).
+ selectedMods.Value = Preset.Value.Mods.ToArray();
+ }
+
+ protected override void Deselect()
{
- // if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections.
// if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset
// (there are no other active mods than what the preset specifies, and the mod settings match exactly).
// therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off.
- selectedMods.Value = !Active.Value
- ? Preset.Value.Mods.ToArray()
- : Array.Empty();
+ selectedMods.Value = Array.Empty();
}
private void selectedModsChanged()
diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs
index 0224631577..d5dc079628 100644
--- a/osu.Game/Overlays/Mods/ModSelectColumn.cs
+++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs
@@ -159,12 +159,15 @@ namespace osu.Game.Overlays.Mods
int wordIndex = 0;
- headerText.AddText(text, t =>
+ ITextPart part = headerText.AddText(text, t =>
{
if (wordIndex == 0)
t.Font = t.Font.With(weight: FontWeight.SemiBold);
wordIndex += 1;
});
+
+ // Reset the index so that if the parts are refreshed (e.g. through changes in localisation) the correct word is re-emboldened.
+ part.DrawablePartsRecreated += _ => wordIndex = 0;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index adc008e1f7..b993aca0ca 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Mods
{
if (AllowCustomisation)
{
- yield return customisationButton = new ShearedToggleButton(BUTTON_WIDTH)
+ yield return CustomisationButton = new ShearedToggleButton(BUTTON_WIDTH)
{
Text = ModSelectOverlayStrings.ModCustomisation,
Active = { BindTarget = customisationVisible }
@@ -107,11 +107,11 @@ namespace osu.Game.Overlays.Mods
private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer footerButtonFlow = null!;
- private ShearedButton backButton = null!;
private DifficultyMultiplierDisplay? multiplierDisplay;
- private ShearedToggleButton? customisationButton;
+ protected ShearedButton BackButton { get; private set; } = null!;
+ protected ShearedToggleButton? CustomisationButton { get; private set; }
private Sample? columnAppearSample;
@@ -214,7 +214,7 @@ namespace osu.Game.Overlays.Mods
Horizontal = 70
},
Spacing = new Vector2(10),
- ChildrenEnumerable = CreateFooterButtons().Prepend(backButton = new ShearedButton(BUTTON_WIDTH)
+ ChildrenEnumerable = CreateFooterButtons().Prepend(BackButton = new ShearedButton(BUTTON_WIDTH)
{
Text = CommonStrings.Back,
Action = Hide,
@@ -247,8 +247,8 @@ namespace osu.Game.Overlays.Mods
modSettingChangeTracker?.Dispose();
updateMultiplier();
- updateCustomisation(val);
updateFromExternalSelection();
+ updateCustomisation();
if (AllowCustomisation)
{
@@ -356,25 +356,26 @@ namespace osu.Game.Overlays.Mods
multiplierDisplay.Current.Value = multiplier;
}
- private void updateCustomisation(ValueChangedEvent> valueChangedEvent)
+ private void updateCustomisation()
{
- if (customisationButton == null)
+ if (CustomisationButton == null)
return;
- bool anyCustomisableMod = false;
- bool anyModWithRequiredCustomisationAdded = false;
+ bool anyCustomisableModActive = false;
+ bool anyModPendingConfiguration = false;
- foreach (var mod in SelectedMods.Value)
+ foreach (var modState in allAvailableMods)
{
- anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
- anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration;
+ anyCustomisableModActive |= modState.Active.Value && modState.Mod.GetSettingsSourceProperties().Any();
+ anyModPendingConfiguration |= modState.PendingConfiguration;
+ modState.PendingConfiguration = false;
}
- if (anyCustomisableMod)
+ if (anyCustomisableModActive)
{
customisationVisible.Disabled = false;
- if (anyModWithRequiredCustomisationAdded && !customisationVisible.Value)
+ if (anyModPendingConfiguration && !customisationVisible.Value)
customisationVisible.Value = true;
}
else
@@ -394,7 +395,7 @@ namespace osu.Game.Overlays.Mods
foreach (var button in footerButtonFlow)
{
- if (button != customisationButton)
+ if (button != CustomisationButton)
button.Enabled.Value = !customisationVisible.Value;
}
@@ -587,14 +588,14 @@ namespace osu.Game.Overlays.Mods
{
if (customisationVisible.Value)
{
- Debug.Assert(customisationButton != null);
- customisationButton.TriggerClick();
+ Debug.Assert(CustomisationButton != null);
+ CustomisationButton.TriggerClick();
if (!immediate)
return;
}
- backButton.TriggerClick();
+ BackButton.TriggerClick();
}
}
@@ -708,7 +709,18 @@ namespace osu.Game.Overlays.Mods
FinishTransforms();
}
- protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || (Column as ModColumn)?.SelectionAnimationRunning == true;
+ protected override bool RequiresChildrenUpdate
+ {
+ get
+ {
+ bool result = base.RequiresChildrenUpdate;
+
+ if (Column is ModColumn modColumn)
+ result |= !modColumn.ItemsLoaded || modColumn.SelectionAnimationRunning;
+
+ return result;
+ }
+ }
private void updateState()
{
diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs
index b3df00f8f9..27abface0c 100644
--- a/osu.Game/Overlays/Mods/ModSelectPanel.cs
+++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs
@@ -143,9 +143,25 @@ namespace osu.Game.Overlays.Mods
}
};
- Action = () => Active.Toggle();
+ Action = () =>
+ {
+ if (!Active.Value)
+ Select();
+ else
+ Deselect();
+ };
}
+ ///
+ /// Performs all actions necessary to select this .
+ ///
+ protected abstract void Select();
+
+ ///
+ /// Performs all actions necessary to deselect this .
+ ///
+ protected abstract void Deselect();
+
[BackgroundDependencyLoader]
private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler)
{
diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs
index 79880b85a5..3ee890e876 100644
--- a/osu.Game/Overlays/Mods/ModState.cs
+++ b/osu.Game/Overlays/Mods/ModState.cs
@@ -24,6 +24,13 @@ namespace osu.Game.Overlays.Mods
///
public BindableBool Active { get; } = new BindableBool();
+ ///
+ /// Whether the mod requires further customisation.
+ /// This flag is read by the to determine if the customisation panel should be opened after a mod change
+ /// and cleared after reading.
+ ///
+ public bool PendingConfiguration { get; set; }
+
///
/// Whether the mod is currently filtered out due to not matching imposed criteria.
///
diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
index 7e079c8341..67a6df3228 100644
--- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@@ -70,85 +71,90 @@ namespace osu.Game.Overlays.Profile.Header
Masking = true,
CornerRadius = avatar_size * 0.25f,
},
- new Container
+ new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
- Padding = new MarginPadding { Left = 10 },
- Children = new Drawable[]
+ Child = new Container
{
- new FillFlowContainer
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Padding = new MarginPadding { Left = 10 },
+ Children = new Drawable[]
{
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ new FillFlowContainer
{
- new FillFlowContainer
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Children = new Drawable[]
+ new FillFlowContainer
{
- usernameText = new OsuSpriteText
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
{
- Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
- },
- openUserExternally = new ExternalLinkButton
- {
- Margin = new MarginPadding { Left = 5 },
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- }
- },
- titleText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
- },
- }
- },
- new FillFlowContainer
- {
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.BottomLeft,
- Direction = FillDirection.Vertical,
- AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- supporterTag = new SupporterIcon
- {
- Height = 20,
- Margin = new MarginPadding { Top = 5 }
- },
- new Box
- {
- RelativeSizeAxes = Axes.X,
- Height = 1.5f,
- Margin = new MarginPadding { Top = 10 },
- Colour = colourProvider.Light1,
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Top = 5 },
- Direction = FillDirection.Horizontal,
- Children = new Drawable[]
- {
- userFlag = new UpdateableFlag
- {
- Size = new Vector2(28, 20),
- ShowPlaceholderOnUnknown = false,
- },
- userCountryText = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
- Margin = new MarginPadding { Left = 10 },
- Origin = Anchor.CentreLeft,
- Anchor = Anchor.CentreLeft,
- Colour = colourProvider.Light1,
+ usernameText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
+ },
+ openUserExternally = new ExternalLinkButton
+ {
+ Margin = new MarginPadding { Left = 5 },
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
}
- }
- },
+ },
+ titleText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
+ },
+ }
+ },
+ new FillFlowContainer
+ {
+ Origin = Anchor.BottomLeft,
+ Anchor = Anchor.BottomLeft,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ supporterTag = new SupporterIcon
+ {
+ Height = 20,
+ Margin = new MarginPadding { Top = 5 }
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 1.5f,
+ Margin = new MarginPadding { Top = 10 },
+ Colour = colourProvider.Light1,
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Margin = new MarginPadding { Top = 5 },
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ userFlag = new UpdateableFlag
+ {
+ Size = new Vector2(28, 20),
+ ShowPlaceholderOnUnknown = false,
+ },
+ userCountryText = new OsuSpriteText
+ {
+ Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
+ Margin = new MarginPadding { Left = 10 },
+ Origin = Anchor.CentreLeft,
+ Anchor = Anchor.CentreLeft,
+ Colour = colourProvider.Light1,
+ }
+ }
+ },
+ }
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs
index 23f91fba4b..a0f069b3bb 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.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.Localisation;
using osu.Game.Rulesets;
diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs
index 30fa1ea8cb..1f9e26c9d7 100644
--- a/osu.Game/Rulesets/Mods/IMod.cs
+++ b/osu.Game/Rulesets/Mods/IMod.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mods
{
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Mods
///
/// The user readable description of this mod.
///
- string Description { get; }
+ LocalisableString Description { get; }
///
/// The type of this mod.
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 0f3d758f74..e4c91d3037 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -9,6 +9,7 @@ using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.UI;
@@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Mods
public virtual ModType Type => ModType.Fun;
[JsonIgnore]
- public abstract string Description { get; }
+ public abstract LocalisableString Description { get; }
///
/// The tooltip to display for this mod when used in a .
diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
index 7b84db844b..697b303689 100644
--- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
+++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Bindables;
+using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "AS";
- public override string Description => "Let track speed adapt to you.";
+ public override LocalisableString Description => "Let track speed adapt to you.";
public override ModType Type => ModType.Fun;
diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs
index ab49dd5575..6cafe0716d 100644
--- a/osu.Game/Rulesets/Mods/ModAutoplay.cs
+++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Replays;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "AT";
public override IconUsage? Icon => OsuIcon.ModAuto;
public override ModType Type => ModType.Automation;
- public override string Description => "Watch a perfect automated play through the song.";
+ public override LocalisableString Description => "Watch a perfect automated play through the song.";
public override double ScoreMultiplier => 1;
public bool PerformFail() => false;
diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs
index bacb953f76..0c301d293f 100644
--- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs
+++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
@@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Barrel Roll";
public override string Acronym => "BR";
- public override string Description => "The whole playfield is on a wheel!";
+ public override LocalisableString Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1;
public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs
index 99c4e71d1f..ae661c5f25 100644
--- a/osu.Game/Rulesets/Mods/ModCinema.cs
+++ b/osu.Game/Rulesets/Mods/ModCinema.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
@@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Cinema";
public override string Acronym => "CN";
public override IconUsage? Icon => OsuIcon.ModCinema;
- public override string Description => "Watch the video without visual distractions.";
+ public override LocalisableString Description => "Watch the video without visual distractions.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAutoplay)).ToArray();
diff --git a/osu.Game/Rulesets/Mods/ModClassic.cs b/osu.Game/Rulesets/Mods/ModClassic.cs
index 1159955e11..55b16297e2 100644
--- a/osu.Game/Rulesets/Mods/ModClassic.cs
+++ b/osu.Game/Rulesets/Mods/ModClassic.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mods
{
@@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => FontAwesome.Solid.History;
- public override string Description => "Feeling nostalgic?";
+ public override LocalisableString Description => "Feeling nostalgic?";
public override ModType Type => ModType.Conversion;
}
diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs
index 9e8e44229e..de1a5ab56c 100644
--- a/osu.Game/Rulesets/Mods/ModDaycore.cs
+++ b/osu.Game/Rulesets/Mods/ModDaycore.cs
@@ -4,6 +4,7 @@
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mods
{
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Daycore";
public override string Acronym => "DC";
public override IconUsage? Icon => null;
- public override string Description => "Whoaaaaa...";
+ public override LocalisableString Description => "Whoaaaaa...";
private readonly BindableNumber tempoAdjust = new BindableDouble(1);
private readonly BindableNumber freqAdjust = new BindableDouble(1);
diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
index b7435ec3ec..f4c6be4f77 100644
--- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
{
public override string Name => @"Difficulty Adjust";
- public override string Description => @"Override a beatmap's difficulty settings.";
+ public override LocalisableString Description => @"Override a beatmap's difficulty settings.";
public override string Acronym => "DA";
diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
index 1c71f5d055..d8a41ae658 100644
--- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs
+++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
@@ -3,6 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "DT";
public override IconUsage? Icon => OsuIcon.ModDoubleTime;
public override ModType Type => ModType.DifficultyIncrease;
- public override string Description => "Zoooooooooom...";
+ public override LocalisableString Description => "Zoooooooooom...";
[SettingSource("Speed increase", "The actual increase to apply")]
public override BindableNumber SpeedChange { get; } = new BindableDouble
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index 210dd56137..558605efc3 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
@@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "FL";
public override IconUsage? Icon => OsuIcon.ModFlashlight;
public override ModType Type => ModType.DifficultyIncrease;
- public override string Description => "Restricted view area.";
+ public override LocalisableString Description => "Restricted view area.";
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public abstract BindableFloat SizeMultiplier { get; }
diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs
index 13d89e30d6..8d8b97e79e 100644
--- a/osu.Game/Rulesets/Mods/ModHalfTime.cs
+++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs
@@ -3,6 +3,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "HT";
public override IconUsage? Icon => OsuIcon.ModHalftime;
public override ModType Type => ModType.DifficultyReduction;
- public override string Description => "Less zoom...";
+ public override LocalisableString Description => "Less zoom...";
[SettingSource("Speed decrease", "The actual decrease to apply")]
public override BindableNumber SpeedChange { get; } = new BindableDouble
diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs
index 0a5348a8cf..2886e59c54 100644
--- a/osu.Game/Rulesets/Mods/ModHardRock.cs
+++ b/osu.Game/Rulesets/Mods/ModHardRock.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "HR";
public override IconUsage? Icon => OsuIcon.ModHardRock;
public override ModType Type => ModType.DifficultyIncrease;
- public override string Description => "Everything just got a bit harder...";
+ public override LocalisableString Description => "Everything just got a bit harder...";
public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) };
public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs
index 55d5abfa82..9735d6b536 100644
--- a/osu.Game/Rulesets/Mods/ModMuted.cs
+++ b/osu.Game/Rulesets/Mods/ModMuted.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Muted";
public override string Acronym => "MU";
public override IconUsage? Icon => FontAwesome.Solid.VolumeMute;
- public override string Description => "Can you still feel the rhythm without music?";
+ public override LocalisableString Description => "Can you still feel the rhythm without music?";
public override ModType Type => ModType.Fun;
public override double ScoreMultiplier => 1;
}
diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs
index c4417ec509..099bf386f3 100644
--- a/osu.Game/Rulesets/Mods/ModNightcore.cs
+++ b/osu.Game/Rulesets/Mods/ModNightcore.cs
@@ -7,6 +7,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Nightcore";
public override string Acronym => "NC";
public override IconUsage? Icon => OsuIcon.ModNightcore;
- public override string Description => "Uguuuuuuuu...";
+ public override LocalisableString Description => "Uguuuuuuuu...";
}
public abstract class ModNightcore : ModNightcore, IApplicableToDrawableRuleset
diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs
index 5ebae17228..31bb4338b3 100644
--- a/osu.Game/Rulesets/Mods/ModNoFail.cs
+++ b/osu.Game/Rulesets/Mods/ModNoFail.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "NF";
public override IconUsage? Icon => OsuIcon.ModNoFail;
public override ModType Type => ModType.DifficultyReduction;
- public override string Description => "You can't fail, no matter what.";
+ public override LocalisableString Description => "You can't fail, no matter what.";
public override double ScoreMultiplier => 0.5;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) };
}
diff --git a/osu.Game/Rulesets/Mods/ModNoMod.cs b/osu.Game/Rulesets/Mods/ModNoMod.cs
index 1009c5bc42..5dd4b317e7 100644
--- a/osu.Game/Rulesets/Mods/ModNoMod.cs
+++ b/osu.Game/Rulesets/Mods/ModNoMod.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mods
{
@@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
{
public override string Name => "No Mod";
public override string Acronym => "NM";
- public override string Description => "No mods applied.";
+ public override LocalisableString Description => "No mods applied.";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Ban;
public override ModType Type => ModType.System;
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index 9016a24f8d..804f23b6b7 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override IconUsage? Icon => OsuIcon.ModPerfect;
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1;
- public override string Description => "SS or quit.";
+ public override LocalisableString Description => "SS or quit.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray();
diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
index c8b835f78a..4e4e8662e8 100644
--- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
+++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
public override string Acronym => "SD";
public override IconUsage? Icon => OsuIcon.ModSuddenDeath;
public override ModType Type => ModType.DifficultyIncrease;
- public override string Description => "Miss and fail.";
+ public override LocalisableString Description => "Miss and fail.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();
diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
index 08bd44f7bd..e84bdab69c 100644
--- a/osu.Game/Rulesets/Mods/ModWindDown.cs
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
{
public override string Name => "Wind Down";
public override string Acronym => "WD";
- public override string Description => "Sloooow doooown...";
+ public override LocalisableString Description => "Sloooow doooown...";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown;
public override double ScoreMultiplier => 1.0;
diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs
index df8f781148..39cee50f96 100644
--- a/osu.Game/Rulesets/Mods/ModWindUp.cs
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
@@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
{
public override string Name => "Wind Up";
public override string Acronym => "WU";
- public override string Description => "Can you keep up?";
+ public override LocalisableString Description => "Can you keep up?";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp;
public override double ScoreMultiplier => 1.0;
diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs
index 1c41c6b8b3..9fbc0ddd97 100644
--- a/osu.Game/Rulesets/Mods/MultiMod.cs
+++ b/osu.Game/Rulesets/Mods/MultiMod.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mods
{
@@ -10,7 +11,7 @@ namespace osu.Game.Rulesets.Mods
{
public override string Name => string.Empty;
public override string Acronym => string.Empty;
- public override string Description => string.Empty;
+ public override LocalisableString Description => string.Empty;
public override double ScoreMultiplier => 0;
public Mod[] Mods { get; }
diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs
index 72de0ad653..abe05996ff 100644
--- a/osu.Game/Rulesets/Mods/UnknownMod.cs
+++ b/osu.Game/Rulesets/Mods/UnknownMod.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Localisation;
+
namespace osu.Game.Rulesets.Mods
{
public class UnknownMod : Mod
@@ -12,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => $"Unknown mod ({OriginalAcronym})";
public override string Acronym => $"{OriginalAcronym}??";
- public override string Description => "This mod could not be resolved by the game.";
+ public override LocalisableString Description => "This mod could not be resolved by the game.";
public override double ScoreMultiplier => 0;
public override bool UserPlayable => false;
diff --git a/osu.Game/Rulesets/RealmRulesetStore.cs b/osu.Game/Rulesets/RealmRulesetStore.cs
index 62a7a13c07..dba7f47f2f 100644
--- a/osu.Game/Rulesets/RealmRulesetStore.cs
+++ b/osu.Game/Rulesets/RealmRulesetStore.cs
@@ -5,8 +5,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.ObjectExtensions;
-using osu.Framework.Logging;
using osu.Framework.Platform;
+using osu.Game.Beatmaps;
using osu.Game.Database;
namespace osu.Game.Rulesets
@@ -68,8 +68,14 @@ namespace osu.Game.Rulesets
{
try
{
- var resolvedType = Type.GetType(r.InstantiationInfo)
- ?? throw new RulesetLoadException(@"Type could not be resolved");
+ var resolvedType = Type.GetType(r.InstantiationInfo);
+
+ if (resolvedType == null)
+ {
+ // ruleset DLL was probably deleted.
+ r.Available = false;
+ continue;
+ }
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
?? throw new RulesetLoadException(@"Instantiation failure");
@@ -83,17 +89,35 @@ namespace osu.Game.Rulesets
r.InstantiationInfo = instanceInfo.InstantiationInfo;
r.Available = true;
+ testRulesetCompatibility(r);
+
detachedRulesets.Add(r.Clone());
}
catch (Exception ex)
{
r.Available = false;
- Logger.Log($"Could not load ruleset {r}: {ex.Message}");
+ LogFailedLoad(r.Name, ex);
}
}
availableRulesets.AddRange(detachedRulesets.OrderBy(r => r));
});
}
+
+ private void testRulesetCompatibility(RulesetInfo rulesetInfo)
+ {
+ // do various operations to ensure that we are in a good state.
+ // if we can avoid loading the ruleset at this point (rather than erroring later in runtime) then that is preferred.
+ var instance = rulesetInfo.CreateInstance();
+
+ instance.CreateAllMods();
+ instance.CreateIcon();
+ instance.CreateResourceStore();
+
+ var beatmap = new Beatmap();
+ var converter = instance.CreateBeatmapConverter(beatmap);
+
+ instance.CreateBeatmapProcessor(converter.Convert());
+ }
}
}
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index c1ec6c30ef..0968d78ed7 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -28,6 +28,7 @@ using osu.Game.Users;
using JetBrains.Annotations;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Extensions;
using osu.Game.Rulesets.Filter;
@@ -288,7 +289,7 @@ namespace osu.Game.Rulesets
///
/// The variant.
/// A descriptive name of the variant.
- public virtual string GetVariantName(int variant) => string.Empty;
+ public virtual LocalisableString GetVariantName(int variant) => string.Empty;
///
/// For rulesets which support legacy (osu-stable) replay conversion, this method will create an empty replay frame
@@ -313,7 +314,7 @@ namespace osu.Game.Rulesets
///
/// All valid s along with a display-friendly name.
///
- public IEnumerable<(HitResult result, string displayName)> GetHitResults()
+ public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResults()
{
var validResults = GetValidHitResults();
@@ -351,7 +352,7 @@ namespace osu.Game.Rulesets
///
/// The result type to get the name for.
/// The display name.
- public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription();
+ public virtual LocalisableString GetDisplayNameForHitResult(HitResult result) => result.GetLocalisableDescription();
///
/// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 6b3e43cc1c..fdbcd0ed1e 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -138,7 +138,7 @@ namespace osu.Game.Rulesets
}
catch (Exception e)
{
- Logger.Error(e, $"Failed to load ruleset {filename}");
+ LogFailedLoad(filename, e);
}
}
@@ -158,7 +158,7 @@ namespace osu.Game.Rulesets
}
catch (Exception e)
{
- Logger.Error(e, $"Failed to add ruleset {assembly}");
+ LogFailedLoad(assembly.FullName, e);
}
}
@@ -173,6 +173,12 @@ namespace osu.Game.Rulesets
AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
}
+ protected void LogFailedLoad(string name, Exception exception)
+ {
+ Logger.Log($"Could not load ruleset {name}. Please check for an update from the developer.", level: LogLevel.Error);
+ Logger.Log($"Ruleset load failed: {exception}");
+ }
+
#region Implementation of IRulesetStore
IRulesetInfo? IRulesetStore.GetRuleset(int id) => GetRuleset(id);
diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs
index bfa256fc20..5047fdea82 100644
--- a/osu.Game/Rulesets/Scoring/HitResult.cs
+++ b/osu.Game/Rulesets/Scoring/HitResult.cs
@@ -119,8 +119,20 @@ namespace osu.Game.Rulesets.Scoring
[EnumMember(Value = "ignore_hit")]
[Order(12)]
IgnoreHit,
+
+ ///
+ /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy).
+ ///
+ ///
+ /// DO NOT USE.
+ ///
+ [EnumMember(Value = "legacy_combo_increase")]
+ [Order(99)]
+ [Obsolete("Do not use.")]
+ LegacyComboIncrease = 99
}
+#pragma warning disable CS0618
public static class HitResultExtensions
{
///
@@ -150,6 +162,7 @@ namespace osu.Game.Rulesets.Scoring
case HitResult.Perfect:
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
+ case HitResult.LegacyComboIncrease:
return true;
default:
@@ -161,13 +174,25 @@ namespace osu.Game.Rulesets.Scoring
/// Whether a affects the accuracy portion of the score.
///
public static bool AffectsAccuracy(this HitResult result)
- => IsScorable(result) && !IsBonus(result);
+ {
+ // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
+ if (result == HitResult.LegacyComboIncrease)
+ return false;
+
+ return IsScorable(result) && !IsBonus(result);
+ }
///
/// Whether a is a non-tick and non-bonus result.
///
public static bool IsBasic(this HitResult result)
- => IsScorable(result) && !IsTick(result) && !IsBonus(result);
+ {
+ // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
+ if (result == HitResult.LegacyComboIncrease)
+ return false;
+
+ return IsScorable(result) && !IsTick(result) && !IsBonus(result);
+ }
///
/// Whether a should be counted as a tick.
@@ -225,12 +250,19 @@ namespace osu.Game.Rulesets.Scoring
///
/// Whether a is scorable.
///
- public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss;
+ public static bool IsScorable(this HitResult result)
+ {
+ // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output.
+ if (result == HitResult.LegacyComboIncrease)
+ return true;
+
+ return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
+ }
///
/// An array of all scorable s.
///
- public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).ToArray();
+ public static readonly HitResult[] ALL_TYPES = ((HitResult[])Enum.GetValues(typeof(HitResult))).Except(new[] { HitResult.LegacyComboIncrease }).ToArray();
///
/// Whether a is valid within a given range.
@@ -251,4 +283,5 @@ namespace osu.Game.Rulesets.Scoring
return result > minResult && result < maxResult;
}
}
+#pragma warning restore CS0618
}
diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
index 12fe0056bb..bc8f2c22f3 100644
--- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
@@ -61,6 +61,11 @@ namespace osu.Game.Rulesets.Scoring
/// The to apply.
public void ApplyResult(JudgementResult result)
{
+#pragma warning disable CS0618
+ if (result.Type == HitResult.LegacyComboIncrease)
+ throw new ArgumentException(@$"A {nameof(HitResult.LegacyComboIncrease)} hit result cannot be applied.");
+#pragma warning restore CS0618
+
JudgedHits++;
lastAppliedResult = result;
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 1deac9f08a..5b21caee84 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -128,8 +128,7 @@ namespace osu.Game.Rulesets.Scoring
private bool beatmapApplied;
private readonly Dictionary scoreResultCounts = new Dictionary();
-
- private Dictionary? maximumResultCounts;
+ private readonly Dictionary maximumResultCounts = new Dictionary();
private readonly List hitEvents = new List();
private HitObject? lastHitObject;
@@ -405,8 +404,6 @@ namespace osu.Game.Rulesets.Scoring
return ScoreRank.D;
}
- public int GetStatistic(HitResult result) => scoreResultCounts.GetValueOrDefault(result);
-
///
/// Resets this ScoreProcessor to a default state.
///
@@ -421,7 +418,9 @@ namespace osu.Game.Rulesets.Scoring
if (storeResults)
{
maximumScoringValues = currentScoringValues;
- maximumResultCounts = new Dictionary(scoreResultCounts);
+
+ maximumResultCounts.Clear();
+ maximumResultCounts.AddRange(scoreResultCounts);
}
scoreResultCounts.Clear();
@@ -449,7 +448,10 @@ namespace osu.Game.Rulesets.Scoring
score.HitEvents = hitEvents;
foreach (var result in HitResultExtensions.ALL_TYPES)
- score.Statistics[result] = GetStatistic(result);
+ score.Statistics[result] = scoreResultCounts.GetValueOrDefault(result);
+
+ foreach (var result in HitResultExtensions.ALL_TYPES)
+ score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
// Populate total score after everything else.
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
@@ -534,6 +536,9 @@ namespace osu.Game.Rulesets.Scoring
{
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
current.MaxCombo = scoreInfo.MaxCombo;
+
+ if (scoreInfo.MaximumStatistics.Count > 0)
+ extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
}
///
@@ -589,7 +594,8 @@ namespace osu.Game.Rulesets.Scoring
if (result.IsBonus())
current.BonusScore += count * Judgement.ToNumericResult(result);
- else
+
+ if (result.AffectsAccuracy())
{
// The maximum result of this judgement if it wasn't a miss.
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
index e41802808f..8f3e077050 100644
--- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.UI
this.gameplayStartTime = gameplayStartTime;
}
- [BackgroundDependencyLoader]
+ [BackgroundDependencyLoader(true)]
private void load(IGameplayClock? gameplayClock)
{
if (gameplayClock != null)
diff --git a/osu.Game/Scoring/HitResultDisplayStatistic.cs b/osu.Game/Scoring/HitResultDisplayStatistic.cs
index 4603ff053e..20deff4875 100644
--- a/osu.Game/Scoring/HitResultDisplayStatistic.cs
+++ b/osu.Game/Scoring/HitResultDisplayStatistic.cs
@@ -3,6 +3,7 @@
#nullable disable
+using osu.Framework.Localisation;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring
@@ -30,9 +31,9 @@ namespace osu.Game.Scoring
///
/// A custom display name for the result type. May be provided by rulesets to give better clarity.
///
- public string DisplayName { get; }
+ public LocalisableString DisplayName { get; }
- public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, string displayName)
+ public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, LocalisableString displayName)
{
Result = result;
Count = count;
diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs
index 0902f1636b..45f827354e 100644
--- a/osu.Game/Scoring/ScoreImporter.cs
+++ b/osu.Game/Scoring/ScoreImporter.cs
@@ -73,6 +73,9 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(model.StatisticsJson))
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
+
+ if (string.IsNullOrEmpty(model.MaximumStatisticsJson))
+ model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics);
}
protected override void PostImport(ScoreInfo model, Realm realm, bool batchImport)
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index d32d611a27..25a7bad9e8 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -63,6 +63,9 @@ namespace osu.Game.Scoring
[MapTo("Statistics")]
public string StatisticsJson { get; set; } = string.Empty;
+ [MapTo("MaximumStatistics")]
+ public string MaximumStatisticsJson { get; set; } = string.Empty;
+
public ScoreInfo(BeatmapInfo? beatmap = null, RulesetInfo? ruleset = null, RealmUser? realmUser = null)
{
Ruleset = ruleset ?? new RulesetInfo();
@@ -133,6 +136,7 @@ namespace osu.Game.Scoring
var clone = (ScoreInfo)this.Detach().MemberwiseClone();
clone.Statistics = new Dictionary(clone.Statistics);
+ clone.MaximumStatistics = new Dictionary(clone.MaximumStatistics);
clone.RealmUser = new RealmUser
{
OnlineID = RealmUser.OnlineID,
@@ -181,6 +185,24 @@ namespace osu.Game.Scoring
set => statistics = value;
}
+ private Dictionary? maximumStatistics;
+
+ [Ignored]
+ public Dictionary MaximumStatistics
+ {
+ get
+ {
+ if (maximumStatistics != null)
+ return maximumStatistics;
+
+ if (!string.IsNullOrEmpty(MaximumStatisticsJson))
+ maximumStatistics = JsonConvert.DeserializeObject>(MaximumStatisticsJson);
+
+ return maximumStatistics ??= new Dictionary();
+ }
+ set => maximumStatistics = value;
+ }
+
private Mod[]? mods;
[Ignored]
diff --git a/osu.Game/Screens/Edit/Setup/ColoursSection.cs b/osu.Game/Screens/Edit/Setup/ColoursSection.cs
index 621abf5580..e3fcdedd1b 100644
--- a/osu.Game/Screens/Edit/Setup/ColoursSection.cs
+++ b/osu.Game/Screens/Edit/Setup/ColoursSection.cs
@@ -7,12 +7,13 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup
{
internal class ColoursSection : SetupSection
{
- public override LocalisableString Title => "Colours";
+ public override LocalisableString Title => EditorSetupStrings.ColoursHeader;
private LabelledColourPalette comboColours;
@@ -23,9 +24,9 @@ namespace osu.Game.Screens.Edit.Setup
{
comboColours = new LabelledColourPalette
{
- Label = "Hitcircle / Slider Combos",
+ Label = EditorSetupStrings.HitCircleSliderCombos,
FixedLabelWidth = LABEL_WIDTH,
- ColourNamePrefix = "Combo"
+ ColourNamePrefix = EditorSetupStrings.ComboColourPrefix
}
};
diff --git a/osu.Game/Screens/Edit/Setup/DesignSection.cs b/osu.Game/Screens/Edit/Setup/DesignSection.cs
index 5cec730440..cc3e9b91ab 100644
--- a/osu.Game/Screens/Edit/Setup/DesignSection.cs
+++ b/osu.Game/Screens/Edit/Setup/DesignSection.cs
@@ -13,6 +13,7 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup
{
@@ -29,7 +30,7 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledSwitchButton letterboxDuringBreaks;
private LabelledSwitchButton samplesMatchPlaybackRate;
- public override LocalisableString Title => "Design";
+ public override LocalisableString Title => EditorSetupStrings.DesignHeader;
[BackgroundDependencyLoader]
private void load()
@@ -38,9 +39,9 @@ namespace osu.Game.Screens.Edit.Setup
{
EnableCountdown = new LabelledSwitchButton
{
- Label = "Enable countdown",
+ Label = EditorSetupStrings.EnableCountdown,
Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None },
- Description = "If enabled, an \"Are you ready? 3, 2, 1, GO!\" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."
+ Description = EditorSetupStrings.CountdownDescription
},
CountdownSettings = new FillFlowContainer
{
@@ -52,41 +53,41 @@ namespace osu.Game.Screens.Edit.Setup
{
CountdownSpeed = new LabelledEnumDropdown
{
- Label = "Countdown speed",
+ Label = EditorSetupStrings.CountdownSpeed,
Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None ? Beatmap.BeatmapInfo.Countdown : CountdownType.Normal },
Items = Enum.GetValues(typeof(CountdownType)).Cast().Where(type => type != CountdownType.None)
},
CountdownOffset = new LabelledNumberBox
{
- Label = "Countdown offset",
+ Label = EditorSetupStrings.CountdownOffset,
Current = { Value = Beatmap.BeatmapInfo.CountdownOffset.ToString() },
- Description = "If the countdown sounds off-time, use this to make it appear one or more beats early.",
+ Description = EditorSetupStrings.CountdownOffsetDescription,
}
}
},
Empty(),
widescreenSupport = new LabelledSwitchButton
{
- Label = "Widescreen support",
- Description = "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.",
+ Label = EditorSetupStrings.WidescreenSupport,
+ Description = EditorSetupStrings.WidescreenSupportDescription,
Current = { Value = Beatmap.BeatmapInfo.WidescreenStoryboard }
},
epilepsyWarning = new LabelledSwitchButton
{
- Label = "Epilepsy warning",
- Description = "Recommended if the storyboard or video contain scenes with rapidly flashing colours.",
+ Label = EditorSetupStrings.EpilepsyWarning,
+ Description = EditorSetupStrings.EpilepsyWarningDescription,
Current = { Value = Beatmap.BeatmapInfo.EpilepsyWarning }
},
letterboxDuringBreaks = new LabelledSwitchButton
{
- Label = "Letterbox during breaks",
- Description = "Adds horizontal letterboxing to give a cinematic look during breaks.",
+ Label = EditorSetupStrings.LetterboxDuringBreaks,
+ Description = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks }
},
samplesMatchPlaybackRate = new LabelledSwitchButton
{
- Label = "Samples match playback rate",
- Description = "When enabled, all samples will speed up or slow down when rate-changing mods are enabled.",
+ Label = EditorSetupStrings.SamplesMatchPlaybackRate,
+ Description = EditorSetupStrings.SamplesMatchPlaybackRateDescription,
Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate }
}
};
diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs
index ce44445683..01e31bd688 100644
--- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs
+++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs
@@ -11,6 +11,7 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Resources.Localisation.Web;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup
{
@@ -21,7 +22,7 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledSliderBar approachRateSlider;
private LabelledSliderBar overallDifficultySlider;
- public override LocalisableString Title => "Difficulty";
+ public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
[BackgroundDependencyLoader]
private void load()
@@ -32,7 +33,7 @@ namespace osu.Game.Screens.Edit.Setup
{
Label = BeatmapsetsStrings.ShowStatsCs,
FixedLabelWidth = LABEL_WIDTH,
- Description = "The size of all hit objects",
+ Description = EditorSetupStrings.CircleSizeDescription,
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
@@ -45,7 +46,7 @@ namespace osu.Game.Screens.Edit.Setup
{
Label = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH,
- Description = "The rate of passive health drain throughout playable time",
+ Description = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
@@ -58,7 +59,7 @@ namespace osu.Game.Screens.Edit.Setup
{
Label = BeatmapsetsStrings.ShowStatsAr,
FixedLabelWidth = LABEL_WIDTH,
- Description = "The speed at which objects are presented to the player",
+ Description = EditorSetupStrings.ApproachRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
@@ -71,7 +72,7 @@ namespace osu.Game.Screens.Edit.Setup
{
Label = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH,
- Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)",
+ Description = EditorSetupStrings.OverallDifficultyDescription,
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
index 928e5bc3b6..1da7a87f83 100644
--- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs
+++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs
@@ -10,6 +10,7 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Resources.Localisation.Web;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup
{
@@ -26,7 +27,7 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledTextBox sourceTextBox;
private LabelledTextBox tagsTextBox;
- public override LocalisableString Title => "Metadata";
+ public override LocalisableString Title => EditorSetupStrings.MetadataHeader;
[BackgroundDependencyLoader]
private void load()
@@ -35,22 +36,22 @@ namespace osu.Game.Screens.Edit.Setup
Children = new[]
{
- ArtistTextBox = createTextBox("Artist",
+ ArtistTextBox = createTextBox(EditorSetupStrings.Artist,
!string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist),
- RomanisedArtistTextBox = createTextBox("Romanised Artist",
+ RomanisedArtistTextBox = createTextBox(EditorSetupStrings.RomanisedArtist,
!string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
Empty(),
- TitleTextBox = createTextBox("Title",
+ TitleTextBox = createTextBox(EditorSetupStrings.Title,
!string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title),
- RomanisedTitleTextBox = createTextBox("Romanised Title",
+ RomanisedTitleTextBox = createTextBox(EditorSetupStrings.RomanisedTitle,
!string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
Empty(),
- creatorTextBox = createTextBox("Creator", metadata.Author.Username),
- difficultyTextBox = createTextBox("Difficulty Name", Beatmap.BeatmapInfo.DifficultyName),
+ creatorTextBox = createTextBox(EditorSetupStrings.Creator, metadata.Author.Username),
+ difficultyTextBox = createTextBox(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName),
sourceTextBox = createTextBox(BeatmapsetsStrings.ShowInfoSource, metadata.Source),
tagsTextBox = createTextBox(BeatmapsetsStrings.ShowInfoTags, metadata.Tags)
};
diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs
index 69953bf659..cbaa2d8b42 100644
--- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs
+++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup
{
@@ -18,7 +19,7 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledFileChooser audioTrackChooser;
private LabelledFileChooser backgroundChooser;
- public override LocalisableString Title => "Resources";
+ public override LocalisableString Title => EditorSetupStrings.ResourcesHeader;
[Resolved]
private MusicController music { get; set; }
@@ -42,13 +43,13 @@ namespace osu.Game.Screens.Edit.Setup
{
backgroundChooser = new LabelledFileChooser(".jpg", ".jpeg", ".png")
{
- Label = "Background",
+ Label = GameplaySettingsStrings.BackgroundHeader,
FixedLabelWidth = LABEL_WIDTH,
TabbableContentContainer = this
},
audioTrackChooser = new LabelledFileChooser(".mp3", ".ogg")
{
- Label = "Audio Track",
+ Label = EditorSetupStrings.AudioTrack,
FixedLabelWidth = LABEL_WIDTH,
TabbableContentContainer = this
},
@@ -143,12 +144,12 @@ namespace osu.Game.Screens.Edit.Setup
private void updatePlaceholderText()
{
audioTrackChooser.Text = audioTrackChooser.Current.Value == null
- ? "Click to select a track"
- : "Click to replace the track";
+ ? EditorSetupStrings.ClickToSelectTrack
+ : EditorSetupStrings.ClickToReplaceTrack;
backgroundChooser.Text = backgroundChooser.Current.Value == null
- ? "Click to select a background image"
- : "Click to replace the background image";
+ ? EditorSetupStrings.ClickToSelectBackground
+ : EditorSetupStrings.ClickToReplaceBackground;
}
}
}
diff --git a/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs b/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs
index db0641feba..d6664e860b 100644
--- a/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs
+++ b/osu.Game/Screens/Edit/Setup/RulesetSetupSection.cs
@@ -5,12 +5,13 @@
using osu.Framework.Localisation;
using osu.Game.Rulesets;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup
{
public abstract class RulesetSetupSection : SetupSection
{
- public sealed override LocalisableString Title => $"Ruleset ({rulesetInfo.Name})";
+ public sealed override LocalisableString Title => EditorSetupStrings.RulesetHeader(rulesetInfo.Name);
private readonly RulesetInfo rulesetInfo;
diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs
index c531f1da90..9486b3728b 100644
--- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs
+++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs
@@ -4,6 +4,7 @@
#nullable disable
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -11,6 +12,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK.Graphics;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup
{
@@ -77,8 +79,8 @@ namespace osu.Game.Screens.Edit.Setup
{
public SetupScreenTitle()
{
- Title = "beatmap setup";
- Description = "change general settings of your beatmap";
+ Title = EditorSetupStrings.BeatmapSetup.ToLower();
+ Description = EditorSetupStrings.BeatmapSetupDescription;
IconTexture = "Icons/Hexacons/social";
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs
deleted file mode 100644
index 34388bf9b1..0000000000
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using osu.Framework.Bindables;
-using osu.Framework.Timing;
-
-namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
-{
- ///
- /// A which catches up using rate adjustment.
- ///
- public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock
- {
- ///
- /// The catch up rate.
- ///
- public const double CATCHUP_RATE = 2;
-
- ///
- /// The source clock.
- ///
- public IFrameBasedClock? Source { get; set; }
-
- public double CurrentTime { get; private set; }
-
- public bool IsRunning { get; private set; }
-
- public void Reset() => CurrentTime = 0;
-
- public void Start() => IsRunning = true;
-
- public void Stop() => IsRunning = false;
-
- void IAdjustableClock.Start()
- {
- // Our running state should only be managed by an ISyncManager, ignore calls from external sources.
- }
-
- void IAdjustableClock.Stop()
- {
- // Our running state should only be managed by an ISyncManager, ignore calls from external sources.
- }
-
- public bool Seek(double position)
- {
- CurrentTime = position;
- return true;
- }
-
- public void ResetSpeedAdjustments()
- {
- }
-
- public double Rate => IsCatchingUp ? CATCHUP_RATE : 1;
-
- double IAdjustableClock.Rate
- {
- get => Rate;
- set => throw new NotSupportedException();
- }
-
- double IClock.Rate => Rate;
-
- public void ProcessFrame()
- {
- ElapsedFrameTime = 0;
- FramesPerSecond = 0;
-
- if (Source == null)
- return;
-
- Source.ProcessFrame();
-
- if (IsRunning)
- {
- double elapsedSource = Source.ElapsedFrameTime;
- double elapsed = elapsedSource * Rate;
-
- CurrentTime += elapsed;
- ElapsedFrameTime = elapsed;
- FramesPerSecond = Source.FramesPerSecond;
- }
- }
-
- public double ElapsedFrameTime { get; private set; }
-
- public double FramesPerSecond { get; private set; }
-
- public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
-
- public Bindable WaitingOnFrames { get; } = new Bindable(true);
-
- public bool IsCatchingUp { get; set; }
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs
deleted file mode 100644
index 194a3bdcc2..0000000000
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using osu.Framework.Bindables;
-using osu.Framework.Timing;
-
-namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
-{
- ///
- /// A clock which is used by s and managed by an .
- ///
- public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
- {
- ///
- /// Starts this .
- ///
- new void Start();
-
- ///
- /// Stops this .
- ///
- new void Stop();
-
- ///
- /// Whether this clock is waiting on frames to continue playback.
- ///
- Bindable WaitingOnFrames { get; }
-
- ///
- /// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
- ///
- ///
- /// Of note, this will be false if this clock is *ahead* of the master clock.
- ///
- bool IsCatchingUp { get; set; }
-
- ///
- /// The source clock
- ///
- IFrameBasedClock Source { set; }
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs
deleted file mode 100644
index 577100d4ba..0000000000
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System;
-using osu.Framework.Bindables;
-using osu.Framework.Timing;
-
-namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
-{
- ///
- /// Manages the synchronisation between one or more s in relation to a master clock.
- ///
- public interface ISyncManager
- {
- ///
- /// An event which is invoked when gameplay is ready to start.
- ///
- event Action ReadyToStart;
-
- ///
- /// The master clock which player clocks should synchronise to.
- ///
- IAdjustableClock MasterClock { get; }
-
- ///
- /// An event which is invoked when the state of is changed.
- ///
- IBindable MasterState { get; }
-
- ///
- /// Adds an to manage.
- ///
- /// The to add.
- void AddPlayerClock(ISpectatorPlayerClock clock);
-
- ///
- /// Removes an , stopping it from being managed by this .
- ///
- /// The to remove.
- void RemovePlayerClock(ISpectatorPlayerClock clock);
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs
index 68eae76030..d351d121c6 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@@ -14,15 +13,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
///
public class MultiSpectatorPlayer : SpectatorPlayer
{
- private readonly Bindable waitingOnFrames = new Bindable(true);
- private readonly ISpectatorPlayerClock spectatorPlayerClock;
+ private readonly SpectatorPlayerClock spectatorPlayerClock;
///
/// Creates a new .
///
/// The score containing the player's replay.
/// The clock controlling the gameplay running state.
- public MultiSpectatorPlayer(Score score, ISpectatorPlayerClock spectatorPlayerClock)
+ public MultiSpectatorPlayer(Score score, SpectatorPlayerClock spectatorPlayerClock)
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
{
this.spectatorPlayerClock = spectatorPlayerClock;
@@ -31,8 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[BackgroundDependencyLoader]
private void load()
{
- spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames);
-
HUDOverlay.PlayerSettingsOverlay.Expire();
HUDOverlay.HoldToQuit.Expire();
}
@@ -40,9 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override void Update()
{
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay.
- CatchUpSpectatorPlayerClock catchUpClock = (CatchUpSpectatorPlayerClock)GameplayClockContainer.SourceClock;
-
- if (catchUpClock.IsRunning)
+ if (GameplayClockContainer.SourceClock.IsRunning)
GameplayClockContainer.Start();
else
GameplayClockContainer.Stop();
@@ -55,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.UpdateAfterChildren();
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
- waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
+ spectatorPlayerClock.WaitingOnFrames = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
index 7ed0be50e5..cb797d7aff 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
@@ -1,16 +1,12 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@@ -42,18 +38,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override UserActivity InitialActivity => new UserActivity.SpectatingMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
[Resolved]
- private OsuColour colours { get; set; }
+ private OsuColour colours { get; set; } = null!;
[Resolved]
- private MultiplayerClient multiplayerClient { get; set; }
+ private MultiplayerClient multiplayerClient { get; set; } = null!;
private readonly PlayerArea[] instances;
- private MasterGameplayClockContainer masterClockContainer;
- private ISyncManager syncManager;
- private PlayerGrid grid;
- private MultiSpectatorLeaderboard leaderboard;
- private PlayerArea currentAudioSource;
- private bool canStartMasterClock;
+ private MasterGameplayClockContainer masterClockContainer = null!;
+ private SpectatorSyncManager syncManager = null!;
+ private PlayerGrid grid = null!;
+ private MultiSpectatorLeaderboard leaderboard = null!;
+ private PlayerArea? currentAudioSource;
private readonly Room room;
private readonly MultiplayerRoomUser[] users;
@@ -78,57 +73,58 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
FillFlowContainer leaderboardFlow;
Container scoreDisplayContainer;
- masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value);
-
- InternalChildren = new[]
+ InternalChildren = new Drawable[]
{
- (Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)),
- masterClockContainer.WithChild(new GridContainer
+ masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
{
- RelativeSizeAxes = Axes.Both,
- RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
- Content = new[]
+ Child = new GridContainer
{
- new Drawable[]
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
+ Content = new[]
{
- scoreDisplayContainer = new Container
+ new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
- },
- },
- new Drawable[]
- {
- new GridContainer
- {
- RelativeSizeAxes = Axes.Both,
- ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
- Content = new[]
+ scoreDisplayContainer = new Container
{
- new Drawable[]
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ },
+ },
+ new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
+ Content = new[]
{
- leaderboardFlow = new FillFlowContainer
+ new Drawable[]
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(5)
- },
- grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
+ leaderboardFlow = new FillFlowContainer
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(5)
+ },
+ grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
+ }
}
}
}
}
}
- })
+ },
+ syncManager = new SpectatorSyncManager(masterClockContainer)
+ {
+ ReadyToStart = performInitialSeek,
+ }
};
for (int i = 0; i < Users.Count; i++)
- {
- grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer));
- syncManager.AddPlayerClock(instances[i].GameplayClock);
- }
+ grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock()));
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users)
{
@@ -161,9 +157,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.LoadComplete();
masterClockContainer.Reset();
-
- syncManager.ReadyToStart += onReadyToStart;
- syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
}
protected override void Update()
@@ -173,7 +166,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
{
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock))
- .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.MasterClock.CurrentTime))
+ .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.CurrentMasterTime))
.FirstOrDefault();
foreach (var instance in instances)
@@ -181,40 +174,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}
}
- private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock)
- => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
+ private bool isCandidateAudioSource(SpectatorPlayerClock? clock)
+ => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames;
- private void onReadyToStart()
+ private void performInitialSeek()
{
// Seek the master clock to the gameplay time.
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
double startTime = instances.Where(i => i.Score != null)
- .SelectMany(i => i.Score.Replay.Frames)
+ .SelectMany(i => i.Score.AsNonNull().Replay.Frames)
.Select(f => f.Time)
.DefaultIfEmpty(0)
.Min();
masterClockContainer.StartTime = startTime;
masterClockContainer.Reset(true);
-
- // Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
- canStartMasterClock = true;
- }
-
- private void onMasterStateChanged(ValueChangedEvent state)
- {
- switch (state.NewValue)
- {
- case MasterClockState.Synchronised:
- if (canStartMasterClock)
- masterClockContainer.Start();
-
- break;
-
- case MasterClockState.TooFarAhead:
- masterClockContainer.Stop();
- break;
- }
}
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
@@ -242,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
var instance = instances.Single(i => i.UserId == userId);
instance.FadeColour(colours.Gray4, 400, Easing.OutQuint);
- syncManager.RemovePlayerClock(instance.GameplayClock);
+ syncManager.RemoveManagedClock(instance.GameplayClock);
}
public override bool OnBackButton()
@@ -256,7 +230,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
return base.OnBackButton();
}
-
- protected virtual MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) => new MasterGameplayClockContainer(beatmap, 0);
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
index 302d04b531..a1fbdc10de 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
@@ -1,17 +1,13 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
@@ -29,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
///
/// Raised after is called on .
///
- public event Action OnGameplayStarted;
+ public event Action? OnGameplayStarted;
///
/// Whether a is loaded in the area.
@@ -42,25 +38,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public readonly int UserId;
///
- /// The used to control the gameplay running state of a loaded .
+ /// The used to control the gameplay running state of a loaded .
///
- [NotNull]
- public readonly ISpectatorPlayerClock GameplayClock = new CatchUpSpectatorPlayerClock();
+ public readonly SpectatorPlayerClock GameplayClock;
///
/// The currently-loaded score.
///
- [CanBeNull]
- public Score Score { get; private set; }
+ public Score? Score { get; private set; }
+
+ [Resolved]
+ private IBindable beatmap { get; set; } = null!;
private readonly BindableDouble volumeAdjustment = new BindableDouble();
private readonly Container gameplayContent;
private readonly LoadingLayer loadingLayer;
- private OsuScreenStack stack;
+ private OsuScreenStack? stack;
- public PlayerArea(int userId, IFrameBasedClock masterClock)
+ public PlayerArea(int userId, SpectatorPlayerClock clock)
{
UserId = userId;
+ GameplayClock = clock;
RelativeSizeAxes = Axes.Both;
Masking = true;
@@ -77,14 +75,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
};
audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
-
- GameplayClock.Source = masterClock;
}
- [Resolved]
- private IBindable beatmap { get; set; }
-
- public void LoadScore([NotNull] Score score)
+ public void LoadScore(Score score)
{
if (Score != null)
throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score.");
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs
new file mode 100644
index 0000000000..7801f22437
--- /dev/null
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs
@@ -0,0 +1,100 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Timing;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
+{
+ ///
+ /// A clock which catches up using rate adjustment.
+ ///
+ public class SpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
+ {
+ ///
+ /// The catch up rate.
+ ///
+ private const double catchup_rate = 2;
+
+ private readonly GameplayClockContainer masterClock;
+
+ public double CurrentTime { get; private set; }
+
+ ///
+ /// Whether this clock is waiting on frames to continue playback.
+ ///
+ public bool WaitingOnFrames { get; set; } = true;
+
+ ///
+ /// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
+ ///
+ ///
+ /// Of note, this will be false if this clock is *ahead* of the master clock.
+ ///
+ public bool IsCatchingUp { get; set; }
+
+ ///
+ /// Whether this spectator clock should be running.
+ /// Use instead of / to control time.
+ ///
+ public bool IsRunning { get; set; }
+
+ public SpectatorPlayerClock(GameplayClockContainer masterClock)
+ {
+ this.masterClock = masterClock;
+ }
+
+ public void Reset() => CurrentTime = 0;
+
+ public void Start()
+ {
+ // Our running state should only be managed by SpectatorSyncManager via IsRunning.
+ }
+
+ public void Stop()
+ {
+ // Our running state should only be managed by an SpectatorSyncManager via IsRunning.
+ }
+
+ public bool Seek(double position)
+ {
+ CurrentTime = position;
+ return true;
+ }
+
+ public void ResetSpeedAdjustments()
+ {
+ }
+
+ public double Rate
+ {
+ get => IsCatchingUp ? catchup_rate : 1;
+ set => throw new NotImplementedException();
+ }
+
+ public void ProcessFrame()
+ {
+ if (IsRunning)
+ {
+ double elapsedSource = masterClock.ElapsedFrameTime;
+ double elapsed = elapsedSource * Rate;
+
+ CurrentTime += elapsed;
+ ElapsedFrameTime = elapsed;
+ FramesPerSecond = masterClock.FramesPerSecond;
+ }
+ else
+ {
+ ElapsedFrameTime = 0;
+ FramesPerSecond = 0;
+ }
+ }
+
+ public double ElapsedFrameTime { get; private set; }
+
+ public double FramesPerSecond { get; private set; }
+
+ public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs
similarity index 66%
rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs
rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs
index 663025923c..8d087aa25c 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs
@@ -1,22 +1,19 @@
// 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.Diagnostics;
using System.Linq;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Timing;
+using osu.Framework.Logging;
+using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
///
- /// A which synchronises de-synced player clocks through catchup.
+ /// Manages the synchronisation between one or more s in relation to a master clock.
///
- public class CatchUpSyncManager : Component, ISyncManager
+ public class SpectatorSyncManager : Component
{
///
/// The offset from the master clock to which player clocks should remain within to be considered in-sync.
@@ -33,40 +30,53 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
///
public const double MAXIMUM_START_DELAY = 15000;
- public event Action ReadyToStart;
+ ///
+ /// An event which is invoked when gameplay is ready to start.
+ ///
+ public Action? ReadyToStart;
+
+ public double CurrentMasterTime => masterClock.CurrentTime;
///
/// The master clock which is used to control the timing of all player clocks clocks.
///
- public IAdjustableClock MasterClock { get; }
-
- public IBindable MasterState => masterState;
+ private readonly GameplayClockContainer masterClock;
///
/// The player clocks.
///
- private readonly List playerClocks = new List();
+ private readonly List playerClocks = new List();
- private readonly Bindable masterState = new Bindable();
+ private MasterClockState masterState = MasterClockState.Synchronised;
private bool hasStarted;
+
private double? firstStartAttemptTime;
- public CatchUpSyncManager(IAdjustableClock master)
+ public SpectatorSyncManager(GameplayClockContainer master)
{
- MasterClock = master;
+ masterClock = master;
}
- public void AddPlayerClock(ISpectatorPlayerClock clock)
+ ///
+ /// Create a new managed .
+ ///
+ /// The newly created .
+ public SpectatorPlayerClock CreateManagedClock()
{
- Debug.Assert(!playerClocks.Contains(clock));
+ var clock = new SpectatorPlayerClock(masterClock);
playerClocks.Add(clock);
+ return clock;
}
- public void RemovePlayerClock(ISpectatorPlayerClock clock)
+ ///
+ /// Removes an , stopping it from being managed by this .
+ ///
+ /// The to remove.
+ public void RemoveManagedClock(SpectatorPlayerClock clock)
{
playerClocks.Remove(clock);
- clock.Stop();
+ clock.IsRunning = false;
}
protected override void Update()
@@ -77,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
// Ensure all player clocks are stopped until the start succeeds.
foreach (var clock in playerClocks)
- clock.Stop();
+ clock.IsRunning = false;
return;
}
@@ -97,7 +107,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
if (playerClocks.Count == 0)
return false;
- int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value);
+ int readyCount = playerClocks.Count(s => !s.WaitingOnFrames);
if (readyCount == playerClocks.Count)
return performStart();
@@ -130,7 +140,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// How far this player's clock is out of sync, compared to the master clock.
// A negative value means the player is running fast (ahead); a positive value means the player is running behind (catching up).
- double timeDelta = MasterClock.CurrentTime - clock.CurrentTime;
+ double timeDelta = masterClock.CurrentTime - clock.CurrentTime;
// Check that the player clock isn't too far ahead.
// This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock.
@@ -139,15 +149,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// Importantly, set the clock to a non-catchup state. if this isn't done, updateMasterState may incorrectly pause the master clock
// when it is required to be running (ie. if all players are ahead of the master).
clock.IsCatchingUp = false;
- clock.Stop();
+ clock.IsRunning = false;
continue;
}
// Make sure the player clock is running if it can.
- if (!clock.WaitingOnFrames.Value)
- clock.Start();
- else
- clock.Stop();
+ clock.IsRunning = !clock.WaitingOnFrames;
if (clock.IsCatchingUp)
{
@@ -169,8 +176,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
///
private void updateMasterState()
{
- bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp);
- masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
+ MasterClockState newState = playerClocks.Any(s => !s.IsCatchingUp) ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
+
+ if (masterState == newState)
+ return;
+
+ masterState = newState;
+ Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {masterState}");
+
+ switch (masterState)
+ {
+ case MasterClockState.Synchronised:
+ if (hasStarted)
+ masterClock.Start();
+
+ break;
+
+ case MasterClockState.TooFarAhead:
+ masterClock.Stop();
+ break;
+ }
}
}
}
diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs
deleted file mode 100644
index b650922173..0000000000
--- a/osu.Game/Screens/Play/GameplayClock.cs
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using System.Collections.Generic;
-using System.Linq;
-using osu.Framework.Bindables;
-using osu.Framework.Timing;
-using osu.Framework.Utils;
-
-namespace osu.Game.Screens.Play
-{
- ///
- /// A clock which is used for gameplay elements that need to follow audio time 1:1.
- /// Exposed via DI by .
- ///
- /// The main purpose of this clock is to stop components using it from accidentally processing the main
- /// , as this should only be done once to ensure accuracy.
- ///
- ///
- public class GameplayClock : IGameplayClock
- {
- internal readonly IFrameBasedClock UnderlyingClock;
-
- public readonly BindableBool IsPaused = new BindableBool();
-
- IBindable IGameplayClock.IsPaused => IsPaused;
-
- public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty();
-
- public GameplayClock(IFrameBasedClock underlyingClock)
- {
- UnderlyingClock = underlyingClock;
- }
-
- public double? StartTime { get; internal set; }
-
- public double CurrentTime => UnderlyingClock.CurrentTime;
-
- public double Rate => UnderlyingClock.Rate;
-
- public double TrueGameplayRate
- {
- get
- {
- double baseRate = Rate;
-
- foreach (double adjustment in NonGameplayAdjustments)
- {
- if (Precision.AlmostEquals(adjustment, 0))
- return 0;
-
- baseRate /= adjustment;
- }
-
- return baseRate;
- }
- }
-
- public bool IsRunning => UnderlyingClock.IsRunning;
-
- public void ProcessFrame()
- {
- // intentionally not updating the underlying clock (handled externally).
- }
-
- public double ElapsedFrameTime => UnderlyingClock.ElapsedFrameTime;
-
- public double FramesPerSecond => UnderlyingClock.FramesPerSecond;
-
- public FrameTimeInfo TimeInfo => UnderlyingClock.TimeInfo;
-
- public IClock Source => UnderlyingClock;
- }
-}
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
index 27b37094ad..ac846b45c4 100644
--- a/osu.Game/Screens/Play/GameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -3,18 +3,19 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Timing;
+using osu.Framework.Utils;
namespace osu.Game.Screens.Play
{
///
- /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use.
+ /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use.
///
public class GameplayClockContainer : Container, IAdjustableClock, IGameplayClock
{
@@ -23,15 +24,8 @@ namespace osu.Game.Screens.Play
///
public IBindable IsPaused => isPaused;
- private readonly BindableBool isPaused = new BindableBool(true);
-
///
- /// The adjustable source clock used for gameplay. Should be used for seeks and clock control.
- ///
- protected readonly DecoupleableInterpolatingFramedClock AdjustableSource;
-
- ///
- /// The source clock.
+ /// The source clock. Should generally not be used for any timekeeping purposes.
///
public IClock SourceClock { get; private set; }
@@ -40,8 +34,6 @@ namespace osu.Game.Screens.Play
///
public event Action? OnSeek;
- private double? startTime;
-
///
/// The time from which the clock should start. Will be seeked to on calling .
///
@@ -49,24 +41,21 @@ namespace osu.Game.Screens.Play
/// If not set, a value of zero will be used.
/// Importantly, the value will be inferred from the current ruleset in unless specified.
///
- public double? StartTime
- {
- get => startTime;
- set
- {
- startTime = value;
+ public double? StartTime { get; set; }
- if (GameplayClock.IsNotNull())
- GameplayClock.StartTime = value;
- }
- }
-
- public IEnumerable NonGameplayAdjustments => GameplayClock.NonGameplayAdjustments;
+ public virtual IEnumerable NonGameplayAdjustments => Enumerable.Empty();
///
/// The final clock which is exposed to gameplay components.
///
- protected GameplayClock GameplayClock { get; private set; } = null!;
+ protected IFrameBasedClock FramedClock { get; private set; }
+
+ private readonly BindableBool isPaused = new BindableBool(true);
+
+ ///
+ /// The adjustable source clock used for gameplay. Should be used for seeks and clock control.
+ ///
+ private readonly DecoupleableInterpolatingFramedClock decoupledClock;
///
/// Creates a new .
@@ -78,21 +67,21 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both;
- AdjustableSource = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
+ decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
IsPaused.BindValueChanged(OnIsPausedChanged);
+
+ // this will be replaced during load, but non-null for tests which don't add this component to the hierarchy.
+ FramedClock = new FramedClock();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- GameplayClock = CreateGameplayClock(AdjustableSource);
+ FramedClock = CreateGameplayClock(decoupledClock);
dependencies.CacheAs(this);
- GameplayClock.StartTime = StartTime;
- GameplayClock.IsPaused.BindTo(isPaused);
-
return dependencies;
}
@@ -103,13 +92,13 @@ namespace osu.Game.Screens.Play
{
ensureSourceClockSet();
- if (!AdjustableSource.IsRunning)
+ if (!decoupledClock.IsRunning)
{
// Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
// This accounts for the clock source potentially taking time to enter a completely stopped state
- Seek(GameplayClock.CurrentTime);
+ Seek(FramedClock.CurrentTime);
- AdjustableSource.Start();
+ decoupledClock.Start();
}
isPaused.Value = false;
@@ -123,10 +112,10 @@ namespace osu.Game.Screens.Play
{
Logger.Log($"{nameof(GameplayClockContainer)} seeking to {time}");
- AdjustableSource.Seek(time);
+ decoupledClock.Seek(time);
// Manually process to make sure the gameplay clock is correctly updated after a seek.
- GameplayClock.UnderlyingClock.ProcessFrame();
+ FramedClock.ProcessFrame();
OnSeek?.Invoke();
}
@@ -143,7 +132,7 @@ namespace osu.Game.Screens.Play
public void Reset(bool startClock = false)
{
// Manually stop the source in order to not affect the IsPaused state.
- AdjustableSource.Stop();
+ decoupledClock.Stop();
if (!IsPaused.Value || startClock)
Start();
@@ -156,10 +145,10 @@ namespace osu.Game.Screens.Play
/// Changes the source clock.
///
/// The new source.
- protected void ChangeSource(IClock sourceClock) => AdjustableSource.ChangeSource(SourceClock = sourceClock);
+ protected void ChangeSource(IClock sourceClock) => decoupledClock.ChangeSource(SourceClock = sourceClock);
///
- /// Ensures that the is set to , if it hasn't been given a source yet.
+ /// Ensures that the is set to , if it hasn't been given a source yet.
/// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode,
/// but not the actual source clock.
/// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor,
@@ -167,39 +156,39 @@ namespace osu.Game.Screens.Play
///
private void ensureSourceClockSet()
{
- if (AdjustableSource.Source == null)
+ if (decoupledClock.Source == null)
ChangeSource(SourceClock);
}
protected override void Update()
{
if (!IsPaused.Value)
- GameplayClock.UnderlyingClock.ProcessFrame();
+ FramedClock.ProcessFrame();
base.Update();
}
///
- /// Invoked when the value of is changed to start or stop the clock.
+ /// Invoked when the value of is changed to start or stop the clock.
///
/// Whether the clock should now be paused.
protected virtual void OnIsPausedChanged(ValueChangedEvent isPaused)
{
if (isPaused.NewValue)
- AdjustableSource.Stop();
+ decoupledClock.Stop();
else
- AdjustableSource.Start();
+ decoupledClock.Start();
}
///
- /// Creates the final which is exposed via DI to be used by gameplay components.
+ /// Creates the final which is exposed via DI to be used by gameplay components.
///
///
/// Any intermediate clocks such as platform offsets should be applied here.
///
/// The providing the source time.
- /// The final .
- protected virtual GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source);
+ /// The final .
+ protected virtual IFrameBasedClock CreateGameplayClock(IFrameBasedClock source) => source;
#region IAdjustableClock
@@ -215,15 +204,15 @@ namespace osu.Game.Screens.Play
double IAdjustableClock.Rate
{
- get => GameplayClock.Rate;
+ get => FramedClock.Rate;
set => throw new NotSupportedException();
}
- public double Rate => GameplayClock.Rate;
+ public double Rate => FramedClock.Rate;
- public double CurrentTime => GameplayClock.CurrentTime;
+ public double CurrentTime => FramedClock.CurrentTime;
- public bool IsRunning => GameplayClock.IsRunning;
+ public bool IsRunning => FramedClock.IsRunning;
#endregion
@@ -232,12 +221,28 @@ namespace osu.Game.Screens.Play
// Handled via update. Don't process here to safeguard from external usages potentially processing frames additional times.
}
- public double ElapsedFrameTime => GameplayClock.ElapsedFrameTime;
+ public double ElapsedFrameTime => FramedClock.ElapsedFrameTime;
- public double FramesPerSecond => GameplayClock.FramesPerSecond;
+ public double FramesPerSecond => FramedClock.FramesPerSecond;
- public FrameTimeInfo TimeInfo => GameplayClock.TimeInfo;
+ public FrameTimeInfo TimeInfo => FramedClock.TimeInfo;
- public double TrueGameplayRate => GameplayClock.TrueGameplayRate;
+ public double TrueGameplayRate
+ {
+ get
+ {
+ double baseRate = Rate;
+
+ foreach (double adjustment in NonGameplayAdjustments)
+ {
+ if (Precision.AlmostEquals(adjustment, 0))
+ return 0;
+
+ baseRate /= adjustment;
+ }
+
+ return baseRate;
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
index 587d2d40a1..d26f0c6311 100644
--- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
@@ -35,8 +35,6 @@ namespace osu.Game.Screens.Play
///
public const double MINIMUM_SKIP_TIME = 1000;
- protected Track Track => (Track)SourceClock;
-
public readonly BindableNumber UserPlaybackRate = new BindableDouble(1)
{
Default = 1,
@@ -51,10 +49,10 @@ namespace osu.Game.Screens.Play
private readonly WorkingBeatmap beatmap;
- private HardwareCorrectionOffsetClock userGlobalOffsetClock = null!;
- private HardwareCorrectionOffsetClock userBeatmapOffsetClock = null!;
- private HardwareCorrectionOffsetClock platformOffsetClock = null!;
- private MasterGameplayClock masterGameplayClock = null!;
+ private OffsetCorrectionClock userGlobalOffsetClock = null!;
+ private OffsetCorrectionClock userBeatmapOffsetClock = null!;
+ private OffsetCorrectionClock platformOffsetClock = null!;
+
private Bindable userAudioOffset = null!;
private IDisposable? beatmapOffsetSubscription;
@@ -67,6 +65,10 @@ namespace osu.Game.Screens.Play
[Resolved]
private OsuConfigManager config { get; set; } = null!;
+ private readonly List> nonGameplayAdjustments = new List>();
+
+ public override IEnumerable NonGameplayAdjustments => nonGameplayAdjustments.Select(b => b.Value);
+
///
/// Create a new master gameplay clock container.
///
@@ -134,7 +136,7 @@ namespace osu.Game.Screens.Play
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ =>
{
if (IsPaused.Value == isPaused.NewValue)
- AdjustableSource.Stop();
+ base.OnIsPausedChanged(isPaused);
});
}
else
@@ -143,14 +145,14 @@ namespace osu.Game.Screens.Play
else
{
if (isPaused.NewValue)
- AdjustableSource.Stop();
+ base.OnIsPausedChanged(isPaused);
// If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations.
pauseFreqAdjust.Value = isPaused.NewValue ? 0 : 1;
// We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment.
// Without doing this, an initial seek may be performed with the wrong offset.
- GameplayClock.UnderlyingClock.ProcessFrame();
+ FramedClock.ProcessFrame();
}
}
@@ -179,29 +181,27 @@ namespace osu.Game.Screens.Play
///
public void Skip()
{
- if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME)
+ if (FramedClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME)
return;
double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME;
- if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
+ if (FramedClock.CurrentTime < 0 && skipTarget > 6000)
// double skip exception for storyboards with very long intros
skipTarget = 0;
Seek(skipTarget);
}
- protected override GameplayClock CreateGameplayClock(IFrameBasedClock source)
+ protected override IFrameBasedClock CreateGameplayClock(IFrameBasedClock source)
{
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
- platformOffsetClock = new HardwareCorrectionOffsetClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
+ platformOffsetClock = new OffsetCorrectionClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
// the final usable gameplay clock with user-set offsets applied.
- userGlobalOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust);
- userBeatmapOffsetClock = new HardwareCorrectionOffsetClock(userGlobalOffsetClock, pauseFreqAdjust);
-
- return masterGameplayClock = new MasterGameplayClock(userBeatmapOffsetClock);
+ userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, pauseFreqAdjust);
+ return userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock, pauseFreqAdjust);
}
///
@@ -221,11 +221,14 @@ namespace osu.Game.Screens.Play
if (speedAdjustmentsApplied)
return;
- Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
- Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
+ if (SourceClock is not Track track)
+ return;
- masterGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust);
- masterGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate);
+ track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
+ track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
+
+ nonGameplayAdjustments.Add(pauseFreqAdjust);
+ nonGameplayAdjustments.Add(UserPlaybackRate);
speedAdjustmentsApplied = true;
}
@@ -235,11 +238,14 @@ namespace osu.Game.Screens.Play
if (!speedAdjustmentsApplied)
return;
- Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
- Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
+ if (SourceClock is not Track track)
+ return;
- masterGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust);
- masterGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate);
+ track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
+ track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate);
+
+ nonGameplayAdjustments.Remove(pauseFreqAdjust);
+ nonGameplayAdjustments.Remove(UserPlaybackRate);
speedAdjustmentsApplied = false;
}
@@ -252,63 +258,8 @@ namespace osu.Game.Screens.Play
}
ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo;
- IClock IBeatSyncProvider.Clock => GameplayClock;
+ IClock IBeatSyncProvider.Clock => this;
+
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => beatmap.TrackLoaded ? beatmap.Track.CurrentAmplitudes : ChannelAmplitudes.Empty;
-
- private class HardwareCorrectionOffsetClock : FramedOffsetClock
- {
- private readonly BindableDouble pauseRateAdjust;
-
- private double offset;
-
- public new double Offset
- {
- get => offset;
- set
- {
- if (value == offset)
- return;
-
- offset = value;
-
- updateOffset();
- }
- }
-
- public double RateAdjustedOffset => base.Offset;
-
- public HardwareCorrectionOffsetClock(IClock source, BindableDouble pauseRateAdjust)
- : base(source)
- {
- this.pauseRateAdjust = pauseRateAdjust;
- }
-
- public override void ProcessFrame()
- {
- base.ProcessFrame();
- updateOffset();
- }
-
- private void updateOffset()
- {
- // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate.
- if (pauseRateAdjust.Value == 1)
- {
- // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this.
- base.Offset = Offset * Rate;
- }
- }
- }
-
- private class MasterGameplayClock : GameplayClock
- {
- public readonly List> MutableNonGameplayAdjustments = new List>();
- public override IEnumerable NonGameplayAdjustments => MutableNonGameplayAdjustments.Select(b => b.Value);
-
- public MasterGameplayClock(FramedOffsetClock underlyingClock)
- : base(underlyingClock)
- {
- }
- }
}
}
diff --git a/osu.Game/Screens/Play/OffsetCorrectionClock.cs b/osu.Game/Screens/Play/OffsetCorrectionClock.cs
new file mode 100644
index 0000000000..207980f45c
--- /dev/null
+++ b/osu.Game/Screens/Play/OffsetCorrectionClock.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Bindables;
+using osu.Framework.Timing;
+
+namespace osu.Game.Screens.Play
+{
+ public class OffsetCorrectionClock : FramedOffsetClock
+ {
+ private readonly BindableDouble pauseRateAdjust;
+
+ private double offset;
+
+ public new double Offset
+ {
+ get => offset;
+ set
+ {
+ if (value == offset)
+ return;
+
+ offset = value;
+
+ updateOffset();
+ }
+ }
+
+ public double RateAdjustedOffset => base.Offset;
+
+ public OffsetCorrectionClock(IClock source, BindableDouble pauseRateAdjust)
+ : base(source)
+ {
+ this.pauseRateAdjust = pauseRateAdjust;
+ }
+
+ public override void ProcessFrame()
+ {
+ base.ProcessFrame();
+ updateOffset();
+ }
+
+ private void updateOffset()
+ {
+ // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate.
+ if (pauseRateAdjust.Value == 1)
+ {
+ // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this.
+ base.Offset = Offset * Rate;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/ResumeOverlay.cs b/osu.Game/Screens/Play/ResumeOverlay.cs
index 2be1f93f80..7ed95c4ce3 100644
--- a/osu.Game/Screens/Play/ResumeOverlay.cs
+++ b/osu.Game/Screens/Play/ResumeOverlay.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
@@ -31,7 +32,7 @@ namespace osu.Game.Screens.Play
protected const float TRANSITION_TIME = 500;
- protected abstract string Message { get; }
+ protected abstract LocalisableString Message { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index 02a95ae9eb..be77304076 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -8,7 +8,6 @@ using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
@@ -84,7 +83,10 @@ namespace osu.Game.Screens.Play
api.Queue(req);
- tcs.Task.WaitSafely();
+ // Generally a timeout would not happen here as APIAccess will timeout first.
+ if (!tcs.Task.Wait(60000))
+ handleTokenFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
+
return true;
void handleTokenFailure(Exception exception)
diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs
index 078ca97737..1505585205 100644
--- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs
+++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs
@@ -8,6 +8,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
@@ -59,7 +60,7 @@ namespace osu.Game.Screens.Ranking.Statistics
private static Drawable createHeader(StatisticItem item)
{
- if (string.IsNullOrEmpty(item.Name))
+ if (LocalisableString.IsNullOrEmpty(item.Name))
return Empty();
return new FillFlowContainer
diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs
index 0e5cce59f8..e3ac054d1b 100644
--- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs
+++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs
@@ -7,6 +7,7 @@ using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Localisation;
namespace osu.Game.Screens.Ranking.Statistics
{
@@ -18,7 +19,7 @@ namespace osu.Game.Screens.Ranking.Statistics
///
/// The name of this item.
///
- public readonly string Name;
+ public readonly LocalisableString Name;
///
/// A function returning the content to be displayed.
@@ -44,11 +45,11 @@ namespace osu.Game.Screens.Ranking.Statistics
///
/// Creates a new , to be displayed inside a in the results screen.
///
- /// The name of the item. Can be to hide the item header.
+ /// The name of the item. Can be to hide the item header.
/// A function returning the content to be displayed.
/// Whether this item requires hit events. If true, will not be called if no hit events are available.
/// The of this item. This can be thought of as the column dimension of an encompassing .
- public StatisticItem([NotNull] string name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null)
+ public StatisticItem(LocalisableString name, [NotNull] Func createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null)
{
Name = name;
RequiresHitEvents = requiresHitEvents;
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index 5b17b412ae..03490ff37b 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -55,8 +55,6 @@ namespace osu.Game.Screens.Select.Carousel
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) ||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
- match &= criteria.Sort != SortMode.DateRanked || BeatmapInfo.BeatmapSet?.DateRanked != null;
-
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
if (match && criteria.SearchTerms.Length > 0)
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
index 59d9318962..6c134a4ab8 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs
@@ -99,6 +99,13 @@ namespace osu.Game.Screens.Select.Carousel
case SortMode.Difficulty:
return compareUsingAggregateMax(otherSet, b => b.StarRating);
+
+ case SortMode.DateSubmitted:
+ // Beatmaps which have no submitted date should already be filtered away in this mode.
+ if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null)
+ return 0;
+
+ return otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
}
}
@@ -122,7 +129,12 @@ namespace osu.Game.Screens.Select.Carousel
public override void Filter(FilterCriteria criteria)
{
base.Filter(criteria);
- Filtered.Value = Items.All(i => i.Filtered.Value);
+ bool match = Items.All(i => i.Filtered.Value);
+
+ match &= criteria.Sort != SortMode.DateRanked || BeatmapSet?.DateRanked != null;
+ match &= criteria.Sort != SortMode.DateSubmitted || BeatmapSet?.DateSubmitted != null;
+
+ Filtered.Value = match;
}
public override string ToString() => BeatmapSet.ToString();
diff --git a/osu.Game/Screens/Select/Filter/SortMode.cs b/osu.Game/Screens/Select/Filter/SortMode.cs
index 1e60ea3bac..c77bdbfbc6 100644
--- a/osu.Game/Screens/Select/Filter/SortMode.cs
+++ b/osu.Game/Screens/Select/Filter/SortMode.cs
@@ -20,6 +20,9 @@ namespace osu.Game.Screens.Select.Filter
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksBpm))]
BPM,
+ [Description("Date Submitted")]
+ DateSubmitted,
+
[Description("Date Added")]
DateAdded,
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 0c2ca6d4af..ece94b5365 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -928,7 +928,7 @@ namespace osu.Game.Screens.Select
}
}
- private class SoloModSelectOverlay : UserModSelectOverlay
+ internal class SoloModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index d67f8415e7..0613db891b 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -31,13 +31,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 463af1143f..bf1e4e350c 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,8 +61,8 @@
-
-
+
+
@@ -84,7 +84,7 @@
-
+