diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 8b5431e2d6..e779ee6658 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -19,3 +19,7 @@ P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResult
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
+M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.
+M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
+M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
+M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead.
diff --git a/osu.Android.props b/osu.Android.props
index 8c15ed7949..3b14d85e53 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Android/AndroidJoystickSettings.cs b/osu.Android/AndroidJoystickSettings.cs
new file mode 100644
index 0000000000..26e921a426
--- /dev/null
+++ b/osu.Android/AndroidJoystickSettings.cs
@@ -0,0 +1,76 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Android.Input;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Localisation;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Android
+{
+ public class AndroidJoystickSettings : SettingsSubsection
+ {
+ protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad;
+
+ private readonly AndroidJoystickHandler joystickHandler;
+
+ private readonly Bindable enabled = new BindableBool(true);
+
+ private SettingsSlider deadzoneSlider = null!;
+
+ private Bindable handlerDeadzone = null!;
+
+ private Bindable localDeadzone = null!;
+
+ public AndroidJoystickSettings(AndroidJoystickHandler joystickHandler)
+ {
+ this.joystickHandler = joystickHandler;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ // use local bindable to avoid changing enabled state of game host's bindable.
+ handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy();
+ localDeadzone = handlerDeadzone.GetUnboundCopy();
+
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = CommonStrings.Enabled,
+ Current = enabled
+ },
+ deadzoneSlider = new SettingsSlider
+ {
+ LabelText = JoystickSettingsStrings.DeadzoneThreshold,
+ KeyboardStep = 0.01f,
+ DisplayAsPercentage = true,
+ Current = localDeadzone,
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ enabled.BindTo(joystickHandler.Enabled);
+ enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true);
+
+ handlerDeadzone.BindValueChanged(val =>
+ {
+ bool disabled = localDeadzone.Disabled;
+
+ localDeadzone.Disabled = false;
+ localDeadzone.Value = val.NewValue;
+ localDeadzone.Disabled = disabled;
+ }, true);
+
+ localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue);
+ }
+ }
+}
diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs
index 636fc7d2df..062f2ce10c 100644
--- a/osu.Android/OsuGameAndroid.cs
+++ b/osu.Android/OsuGameAndroid.cs
@@ -96,6 +96,9 @@ namespace osu.Android
case AndroidMouseHandler mh:
return new AndroidMouseSettings(mh);
+ case AndroidJoystickHandler jh:
+ return new AndroidJoystickSettings(jh);
+
default:
return base.CreateSettingsSubsectionFor(handler);
}
diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj
index 90b02c527b..004cc8c39c 100644
--- a/osu.Android/osu.Android.csproj
+++ b/osu.Android/osu.Android.csproj
@@ -26,6 +26,7 @@
true
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 314a03a73e..524436235e 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -22,10 +22,12 @@ using osu.Framework.Input.Handlers;
using osu.Framework.Input.Handlers.Joystick;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Tablet;
+using osu.Framework.Input.Handlers.Touch;
using osu.Framework.Threading;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Overlays.Settings;
+using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input;
namespace osu.Desktop
@@ -156,6 +158,9 @@ namespace osu.Desktop
case JoystickHandler jh:
return new JoystickSettings(jh);
+ case TouchHandler th:
+ return new InputSection.HandlerSection(th);
+
default:
return base.CreateSettingsSubsectionFor(handler);
}
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index 712f300671..19cf7f5d46 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -14,6 +14,7 @@ using osu.Framework.Platform;
using osu.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
+using SDL2;
using Squirrel;
namespace osu.Desktop
@@ -29,7 +30,27 @@ namespace osu.Desktop
{
// run Squirrel first, as the app may exit after these run
if (OperatingSystem.IsWindows())
+ {
+ var windowsVersion = Environment.OSVersion.Version;
+
+ // While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
+ // See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
+ if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
+ {
+ // If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
+ // disabling it ourselves.
+ // We could also better detect compatibility mode if required:
+ // https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
+ SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
+ "Your operating system is too old to run osu!",
+ "This version of osu! requires at least Windows 8.1 to run.\n"
+ + "Please upgrade your operating system or consider using an older version of osu!.\n\n"
+ + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
+ return;
+ }
+
setupSquirrel();
+ }
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
diff --git a/osu.Game.Benchmarks/BenchmarkHitObject.cs b/osu.Game.Benchmarks/BenchmarkHitObject.cs
new file mode 100644
index 0000000000..65c78e39b3
--- /dev/null
+++ b/osu.Game.Benchmarks/BenchmarkHitObject.cs
@@ -0,0 +1,166 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using BenchmarkDotNet.Attributes;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Benchmarks
+{
+ public class BenchmarkHitObject : BenchmarkTest
+ {
+ [Params(1, 100, 1000)]
+ public int Count { get; set; }
+
+ [Params(false, true)]
+ public bool WithBindableAccess { get; set; }
+
+ [Benchmark]
+ public HitCircle[] OsuCircle()
+ {
+ var circles = new HitCircle[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ circles[i] = new HitCircle();
+
+ if (WithBindableAccess)
+ {
+ _ = circles[i].PositionBindable;
+ _ = circles[i].ScaleBindable;
+ _ = circles[i].ComboIndexBindable;
+ _ = circles[i].ComboOffsetBindable;
+ _ = circles[i].StackHeightBindable;
+ _ = circles[i].LastInComboBindable;
+ _ = circles[i].ComboIndexWithOffsetsBindable;
+ _ = circles[i].IndexInCurrentComboBindable;
+ _ = circles[i].SamplesBindable;
+ _ = circles[i].StartTimeBindable;
+ }
+ else
+ {
+ _ = circles[i].Position;
+ _ = circles[i].Scale;
+ _ = circles[i].ComboIndex;
+ _ = circles[i].ComboOffset;
+ _ = circles[i].StackHeight;
+ _ = circles[i].LastInCombo;
+ _ = circles[i].ComboIndexWithOffsets;
+ _ = circles[i].IndexInCurrentCombo;
+ _ = circles[i].Samples;
+ _ = circles[i].StartTime;
+ _ = circles[i].Position;
+ _ = circles[i].Scale;
+ _ = circles[i].ComboIndex;
+ _ = circles[i].ComboOffset;
+ _ = circles[i].StackHeight;
+ _ = circles[i].LastInCombo;
+ _ = circles[i].ComboIndexWithOffsets;
+ _ = circles[i].IndexInCurrentCombo;
+ _ = circles[i].Samples;
+ _ = circles[i].StartTime;
+ }
+ }
+
+ return circles;
+ }
+
+ [Benchmark]
+ public Hit[] TaikoHit()
+ {
+ var hits = new Hit[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ hits[i] = new Hit();
+
+ if (WithBindableAccess)
+ {
+ _ = hits[i].TypeBindable;
+ _ = hits[i].IsStrongBindable;
+ _ = hits[i].SamplesBindable;
+ _ = hits[i].StartTimeBindable;
+ }
+ else
+ {
+ _ = hits[i].Type;
+ _ = hits[i].IsStrong;
+ _ = hits[i].Samples;
+ _ = hits[i].StartTime;
+ }
+ }
+
+ return hits;
+ }
+
+ [Benchmark]
+ public Fruit[] CatchFruit()
+ {
+ var fruit = new Fruit[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ fruit[i] = new Fruit();
+
+ if (WithBindableAccess)
+ {
+ _ = fruit[i].OriginalXBindable;
+ _ = fruit[i].XOffsetBindable;
+ _ = fruit[i].ScaleBindable;
+ _ = fruit[i].ComboIndexBindable;
+ _ = fruit[i].HyperDashBindable;
+ _ = fruit[i].LastInComboBindable;
+ _ = fruit[i].ComboIndexWithOffsetsBindable;
+ _ = fruit[i].IndexInCurrentComboBindable;
+ _ = fruit[i].IndexInBeatmapBindable;
+ _ = fruit[i].SamplesBindable;
+ _ = fruit[i].StartTimeBindable;
+ }
+ else
+ {
+ _ = fruit[i].OriginalX;
+ _ = fruit[i].XOffset;
+ _ = fruit[i].Scale;
+ _ = fruit[i].ComboIndex;
+ _ = fruit[i].HyperDash;
+ _ = fruit[i].LastInCombo;
+ _ = fruit[i].ComboIndexWithOffsets;
+ _ = fruit[i].IndexInCurrentCombo;
+ _ = fruit[i].IndexInBeatmap;
+ _ = fruit[i].Samples;
+ _ = fruit[i].StartTime;
+ }
+ }
+
+ return fruit;
+ }
+
+ [Benchmark]
+ public Note[] ManiaNote()
+ {
+ var notes = new Note[Count];
+
+ for (int i = 0; i < Count; i++)
+ {
+ notes[i] = new Note();
+
+ if (WithBindableAccess)
+ {
+ _ = notes[i].ColumnBindable;
+ _ = notes[i].SamplesBindable;
+ _ = notes[i].StartTimeBindable;
+ }
+ else
+ {
+ _ = notes[i].Column;
+ _ = notes[i].Samples;
+ _ = notes[i].StartTime;
+ }
+ }
+
+ return notes;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index f5a3426305..6e01c44e1f 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -19,7 +19,9 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public const float OBJECT_RADIUS = 64;
- public readonly Bindable OriginalXBindable = new Bindable();
+ private HitObjectProperty originalX;
+
+ public Bindable OriginalXBindable => originalX.Bindable;
///
/// The horizontal position of the hit object between 0 and .
@@ -31,18 +33,20 @@ namespace osu.Game.Rulesets.Catch.Objects
[JsonIgnore]
public float X
{
- set => OriginalXBindable.Value = value;
+ set => originalX.Value = value;
}
- public readonly Bindable XOffsetBindable = new Bindable();
+ private HitObjectProperty xOffset;
+
+ public Bindable XOffsetBindable => xOffset.Bindable;
///
/// A random offset applied to the horizontal position, set by the beatmap processing.
///
public float XOffset
{
- get => XOffsetBindable.Value;
- set => XOffsetBindable.Value = value;
+ get => xOffset.Value;
+ set => xOffset.Value = value;
}
///
@@ -54,8 +58,8 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float OriginalX
{
- get => OriginalXBindable.Value;
- set => OriginalXBindable.Value = value;
+ get => originalX.Value;
+ set => originalX.Value = value;
}
///
@@ -69,59 +73,71 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TimePreempt { get; set; } = 1000;
- public readonly Bindable IndexInBeatmapBindable = new Bindable();
+ private HitObjectProperty indexInBeatmap;
+
+ public Bindable IndexInBeatmapBindable => indexInBeatmap.Bindable;
public int IndexInBeatmap
{
- get => IndexInBeatmapBindable.Value;
- set => IndexInBeatmapBindable.Value = value;
+ get => indexInBeatmap.Value;
+ set => indexInBeatmap.Value = value;
}
public virtual bool NewCombo { get; set; }
public int ComboOffset { get; set; }
- public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
+ private HitObjectProperty indexInCurrentCombo;
+
+ public Bindable IndexInCurrentComboBindable => indexInCurrentCombo.Bindable;
public int IndexInCurrentCombo
{
- get => IndexInCurrentComboBindable.Value;
- set => IndexInCurrentComboBindable.Value = value;
+ get => indexInCurrentCombo.Value;
+ set => indexInCurrentCombo.Value = value;
}
- public Bindable ComboIndexBindable { get; } = new Bindable();
+ private HitObjectProperty comboIndex;
+
+ public Bindable ComboIndexBindable => comboIndex.Bindable;
public int ComboIndex
{
- get => ComboIndexBindable.Value;
- set => ComboIndexBindable.Value = value;
+ get => comboIndex.Value;
+ set => comboIndex.Value = value;
}
- public Bindable ComboIndexWithOffsetsBindable { get; } = new Bindable();
+ private HitObjectProperty comboIndexWithOffsets;
+
+ public Bindable ComboIndexWithOffsetsBindable => comboIndexWithOffsets.Bindable;
public int ComboIndexWithOffsets
{
- get => ComboIndexWithOffsetsBindable.Value;
- set => ComboIndexWithOffsetsBindable.Value = value;
+ get => comboIndexWithOffsets.Value;
+ set => comboIndexWithOffsets.Value = value;
}
- public Bindable LastInComboBindable { get; } = new Bindable();
+ private HitObjectProperty lastInCombo;
+
+ public Bindable LastInComboBindable => lastInCombo.Bindable;
///
/// The next fruit starts a new combo. Used for explodey.
///
public virtual bool LastInCombo
{
- get => LastInComboBindable.Value;
- set => LastInComboBindable.Value = value;
+ get => lastInCombo.Value;
+ set => lastInCombo.Value = value;
}
- public readonly Bindable ScaleBindable = new Bindable(1);
+ private HitObjectProperty scale = new HitObjectProperty(1);
+
+ public Bindable ScaleBindable => scale.Bindable;
public float Scale
{
- get => ScaleBindable.Value;
- set => ScaleBindable.Value = value;
+ get => scale.Value;
+ set => scale.Value = value;
}
///
diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
index 1ededa1438..c9bc9ca2ac 100644
--- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs
@@ -5,6 +5,7 @@
using Newtonsoft.Json;
using osu.Framework.Bindables;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK.Graphics;
@@ -24,12 +25,14 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float DistanceToHyperDash { get; set; }
- public readonly Bindable HyperDashBindable = new Bindable();
+ private HitObjectProperty hyperDash;
+
+ public Bindable HyperDashBindable => hyperDash.Bindable;
///
/// Whether this fruit can initiate a hyperdash.
///
- public bool HyperDash => HyperDashBindable.Value;
+ public bool HyperDash => hyperDash.Value;
private CatchHitObject hyperDashTarget;
diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 0efaeac026..ebff5cf4e9 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -13,12 +13,14 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
{
- public readonly Bindable ColumnBindable = new Bindable();
+ private HitObjectProperty column;
+
+ public Bindable ColumnBindable => column.Bindable;
public virtual int Column
{
- get => ColumnBindable.Value;
- set => ColumnBindable.Value = value;
+ get => column.Value;
+ set => column.Value = value;
}
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
new file mode 100644
index 0000000000..1aed84be10
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSingleTap.cs
@@ -0,0 +1,175 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Replays;
+using osu.Game.Rulesets.Replays;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModSingleTap : OsuModTestScene
+ {
+ [Test]
+ public void TestInputSingular() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 1500,
+ Position = new Vector2(300, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 2000,
+ Position = new Vector2(400, 100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
+ }
+ });
+
+ [Test]
+ public void TestInputAlternating() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(200, 100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
+ new OsuReplayFrame(1001, new Vector2(200, 100)),
+ new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(1501, new Vector2(300, 100)),
+ new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
+ new OsuReplayFrame(2001, new Vector2(400, 100)),
+ }
+ });
+
+ ///
+ /// Ensures singletapping is reset before the first hitobject after intro.
+ ///
+ [Test]
+ public void TestInputAlternatingAtIntro() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(100),
+ },
+ },
+ },
+ ReplayFrames = new List
+ {
+ // first press during intro.
+ new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(200)),
+ // press different key at hitobject and ensure it has been hit.
+ new OsuReplayFrame(1000, new Vector2(100), OsuAction.RightButton),
+ }
+ });
+
+ ///
+ /// Ensures singletapping is reset before the first hitobject after a break.
+ ///
+ [Test]
+ public void TestInputAlternatingWithBreak() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModSingleTap(),
+ PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
+ Autoplay = false,
+ Beatmap = new Beatmap
+ {
+ Breaks = new List
+ {
+ new BreakPeriod(500, 2000),
+ },
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 500,
+ Position = new Vector2(100),
+ },
+ new HitCircle
+ {
+ StartTime = 2500,
+ Position = new Vector2(500, 100),
+ },
+ new HitCircle
+ {
+ StartTime = 3000,
+ Position = new Vector2(500, 100),
+ },
+ }
+ },
+ ReplayFrames = new List
+ {
+ // first press to start singletap lock.
+ new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(501, new Vector2(100)),
+ // press different key after break but before hit object.
+ new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.RightButton),
+ new OsuReplayFrame(2251, new Vector2(300, 100)),
+ // press same key at second hitobject and ensure it has been hit.
+ new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2501, new Vector2(500, 100)),
+ // press different key at third hitobject and ensure it has been missed.
+ new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.RightButton),
+ new OsuReplayFrame(3001, new Vector2(500, 100)),
+ }
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
new file mode 100644
index 0000000000..a7aca8257b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
+using osu.Game.Utils;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset
+ {
+ public override double ScoreMultiplier => 1.0;
+ public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
+ public override ModType Type => ModType.Conversion;
+
+ private const double flash_duration = 1000;
+
+ private DrawableRuleset ruleset = null!;
+
+ protected OsuAction? LastAcceptedAction { get; private set; }
+
+ ///
+ /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
+ ///
+ ///
+ /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time.
+ ///
+ private PeriodTracker nonGameplayPeriods = null!;
+
+ private IFrameStableClock gameplayClock = null!;
+
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ ruleset = drawableRuleset;
+ drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
+
+ var periods = new List();
+
+ if (drawableRuleset.Objects.Any())
+ {
+ periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
+
+ foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
+ periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
+
+ static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
+ }
+
+ nonGameplayPeriods = new PeriodTracker(periods);
+
+ gameplayClock = drawableRuleset.FrameStableClock;
+ }
+
+ protected abstract bool CheckValidNewAction(OsuAction action);
+
+ private bool checkCorrectAction(OsuAction action)
+ {
+ if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
+ {
+ LastAcceptedAction = null;
+ return true;
+ }
+
+ switch (action)
+ {
+ case OsuAction.LeftButton:
+ case OsuAction.RightButton:
+ break;
+
+ // Any action which is not left or right button should be ignored.
+ default:
+ return true;
+ }
+
+ if (CheckValidNewAction(action))
+ {
+ LastAcceptedAction = action;
+ return true;
+ }
+
+ ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
+ return false;
+ }
+
+ private class InputInterceptor : Component, IKeyBindingHandler
+ {
+ private readonly InputBlockingMod mod;
+
+ public InputInterceptor(InputBlockingMod mod)
+ {
+ this.mod = mod;
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ // if the pressed action is incorrect, block it from reaching gameplay.
+ => !mod.checkCorrectAction(e.Action);
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
index 622d2df432..d88cb17e84 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs
@@ -1,119 +1,20 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
-using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Input.Events;
-using osu.Game.Beatmaps.Timing;
-using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Play;
-using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModAlternate : Mod, IApplicableToDrawableRuleset
+ public class OsuModAlternate : InputBlockingMod
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
- public override double ScoreMultiplier => 1.0;
- public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
- public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
- private const double flash_duration = 1000;
-
- ///
- /// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
- ///
- ///
- /// This is different from in that the periods here end strictly at the first object after the break, rather than the break's end time.
- ///
- private PeriodTracker nonGameplayPeriods;
-
- private OsuAction? lastActionPressed;
- private DrawableRuleset ruleset;
-
- private IFrameStableClock gameplayClock;
-
- public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
- {
- ruleset = drawableRuleset;
- drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
-
- var periods = new List();
-
- if (drawableRuleset.Objects.Any())
- {
- periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
-
- foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
- periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
-
- static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
- }
-
- nonGameplayPeriods = new PeriodTracker(periods);
-
- gameplayClock = drawableRuleset.FrameStableClock;
- }
-
- private bool checkCorrectAction(OsuAction action)
- {
- if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
- {
- lastActionPressed = null;
- return true;
- }
-
- switch (action)
- {
- case OsuAction.LeftButton:
- case OsuAction.RightButton:
- break;
-
- // Any action which is not left or right button should be ignored.
- default:
- return true;
- }
-
- if (lastActionPressed != action)
- {
- // User alternated correctly.
- lastActionPressed = action;
- return true;
- }
-
- ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
- return false;
- }
-
- private class InputInterceptor : Component, IKeyBindingHandler
- {
- private readonly OsuModAlternate mod;
-
- public InputInterceptor(OsuModAlternate mod)
- {
- this.mod = mod;
- }
-
- public bool OnPressed(KeyBindingPressEvent e)
- // if the pressed action is incorrect, block it from reaching gameplay.
- => !mod.checkCorrectAction(e.Action);
-
- public void OnReleased(KeyBindingReleaseEvent e)
- {
- }
- }
+ protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index 490b5b7a9d..c4de77b8a3 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index 656cf95e77..d5096619b9 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema
{
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap), typeof(OsuModRepel) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModPerfect.cs b/osu.Game.Rulesets.Osu/Mods/OsuModPerfect.cs
index c5795177d0..bde7718da5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModPerfect.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModPerfect.cs
@@ -3,11 +3,14 @@
#nullable disable
+using System;
+using System.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModPerfect : ModPerfect
{
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 5c1de83972..2030156f2e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
- public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAlternate) }).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
///
/// How early before a hitobject's start time to trigger a hit.
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
new file mode 100644
index 0000000000..b170d30448
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
@@ -0,0 +1,18 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModSingleTap : InputBlockingMod
+ {
+ public override string Name => @"Single Tap";
+ public override string Acronym => @"SG";
+ public override string Description => @"You must only use one key!";
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
+
+ protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 91bb7f95f6..d83f5df7a3 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -319,13 +319,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
const float fade_out_time = 450;
- // intentionally pile on an extra FadeOut to make it happen much faster.
- Ball.FadeOut(fade_out_time / 4, Easing.Out);
-
switch (state)
{
case ArmedState.Hit:
- Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
index 7bde60b39d..6bfb4e8aae 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
{
+ public const float FOLLOW_AREA = 2.4f;
+
public Func GetInitialHitAction;
public Color4 AccentColour
@@ -31,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
set => ball.Colour = value;
}
- private Drawable followCircle;
private Drawable followCircleReceptor;
private DrawableSlider drawableSlider;
private Drawable ball;
@@ -47,12 +48,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Children = new[]
{
- followCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
+ new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Alpha = 0,
},
followCircleReceptor = new CircularContainer
{
@@ -103,10 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
tracking = value;
- followCircleReceptor.Scale = new Vector2(tracking ? 2.4f : 1f);
-
- followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
- followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
+ followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 387342b4a9..7b98fc48e0 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -7,12 +7,12 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Objects;
-using osuTK;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects
{
@@ -36,12 +36,14 @@ namespace osu.Game.Rulesets.Osu.Objects
public double TimePreempt = 600;
public double TimeFadeIn = 400;
- public readonly Bindable PositionBindable = new Bindable();
+ private HitObjectProperty position;
+
+ public Bindable PositionBindable => position.Bindable;
public virtual Vector2 Position
{
- get => PositionBindable.Value;
- set => PositionBindable.Value = value;
+ get => position.Value;
+ set => position.Value = value;
}
public float X => Position.X;
@@ -53,66 +55,80 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedEndPosition => EndPosition + StackOffset;
- public readonly Bindable StackHeightBindable = new Bindable();
+ private HitObjectProperty stackHeight;
+
+ public Bindable StackHeightBindable => stackHeight.Bindable;
public int StackHeight
{
- get => StackHeightBindable.Value;
- set => StackHeightBindable.Value = value;
+ get => stackHeight.Value;
+ set => stackHeight.Value = value;
}
public virtual Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f);
public double Radius => OBJECT_RADIUS * Scale;
- public readonly Bindable ScaleBindable = new BindableFloat(1);
+ private HitObjectProperty scale = new HitObjectProperty(1);
+
+ public Bindable ScaleBindable => scale.Bindable;
public float Scale
{
- get => ScaleBindable.Value;
- set => ScaleBindable.Value = value;
+ get => scale.Value;
+ set => scale.Value = value;
}
public virtual bool NewCombo { get; set; }
- public readonly Bindable ComboOffsetBindable = new Bindable();
+ private HitObjectProperty comboOffset;
+
+ public Bindable ComboOffsetBindable => comboOffset.Bindable;
public int ComboOffset
{
- get => ComboOffsetBindable.Value;
- set => ComboOffsetBindable.Value = value;
+ get => comboOffset.Value;
+ set => comboOffset.Value = value;
}
- public Bindable IndexInCurrentComboBindable { get; } = new Bindable();
+ private HitObjectProperty indexInCurrentCombo;
+
+ public Bindable IndexInCurrentComboBindable => indexInCurrentCombo.Bindable;
public virtual int IndexInCurrentCombo
{
- get => IndexInCurrentComboBindable.Value;
- set => IndexInCurrentComboBindable.Value = value;
+ get => indexInCurrentCombo.Value;
+ set => indexInCurrentCombo.Value = value;
}
- public Bindable ComboIndexBindable { get; } = new Bindable();
+ private HitObjectProperty comboIndex;
+
+ public Bindable ComboIndexBindable => comboIndex.Bindable;
public virtual int ComboIndex
{
- get => ComboIndexBindable.Value;
- set => ComboIndexBindable.Value = value;
+ get => comboIndex.Value;
+ set => comboIndex.Value = value;
}
- public Bindable ComboIndexWithOffsetsBindable { get; } = new Bindable();
+ private HitObjectProperty comboIndexWithOffsets;
+
+ public Bindable ComboIndexWithOffsetsBindable => comboIndexWithOffsets.Bindable;
public int ComboIndexWithOffsets
{
- get => ComboIndexWithOffsetsBindable.Value;
- set => ComboIndexWithOffsetsBindable.Value = value;
+ get => comboIndexWithOffsets.Value;
+ set => comboIndexWithOffsets.Value = value;
}
- public Bindable LastInComboBindable { get; } = new Bindable();
+ private HitObjectProperty lastInCombo;
+
+ public Bindable LastInComboBindable => lastInCombo.Bindable;
public bool LastInCombo
{
- get => LastInComboBindable.Value;
- set => LastInComboBindable.Value = value;
+ get => lastInCombo.Value;
+ set => lastInCombo.Value = value;
}
protected OsuHitObject()
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index ba0ef9ec3a..302194e91a 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(),
new OsuModRandom(),
new OsuModMirror(),
- new OsuModAlternate(),
+ new MultiMod(new OsuModAlternate(), new OsuModSingleTap())
};
case ModType.Automation:
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
index 8211448705..254e220996 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
@@ -1,19 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
- public class DefaultFollowCircle : CompositeDrawable
+ public class DefaultFollowCircle : FollowCircle
{
public DefaultFollowCircle()
{
- RelativeSizeAxes = Axes.Both;
-
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
@@ -29,5 +29,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
};
}
+
+ protected override void OnTrackingChanged(ValueChangedEvent tracking)
+ {
+ const float scale_duration = 300f;
+ const float fade_duration = 300f;
+
+ this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint)
+ .FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint);
+ }
+
+ protected override void OnSliderEnd()
+ {
+ const float fade_duration = 450f;
+
+ // intentionally pile on an extra FadeOut to make it happen much faster
+ this.FadeOut(fade_duration / 4, Easing.Out);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
index 47308375e6..97bb4a3697 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -19,13 +17,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class DefaultSliderBall : CompositeDrawable
{
- private Box box;
+ private Box box = null!;
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? parentObject { get; set; }
[BackgroundDependencyLoader]
- private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ private void load(ISkinSource skin)
{
- var slider = (DrawableSlider)drawableObject;
-
RelativeSizeAxes = Axes.Both;
float radius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
@@ -51,10 +50,62 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
};
- slider.Tracking.BindValueChanged(trackingChanged, true);
+ if (parentObject != null)
+ {
+ var slider = (DrawableSlider)parentObject;
+ slider.Tracking.BindValueChanged(trackingChanged, true);
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (parentObject != null)
+ {
+ parentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(parentObject, parentObject.State.Value);
+ }
}
private void trackingChanged(ValueChangedEvent tracking) =>
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ const float fade_duration = 450f;
+
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
+ {
+ this.FadeIn()
+ .ScaleTo(1f);
+ }
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
+ // intentionally pile on an extra FadeOut to make it happen much faster
+ this.FadeOut(fade_duration / 4, Easing.Out);
+
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(1.4f, fade_duration, Easing.Out);
+ break;
+ }
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (parentObject != null)
+ parentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs
new file mode 100644
index 0000000000..321705d25e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/FollowCircle.cs
@@ -0,0 +1,75 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public abstract class FollowCircle : CompositeDrawable
+ {
+ [Resolved]
+ protected DrawableHitObject? ParentObject { get; private set; }
+
+ protected FollowCircle()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(OnTrackingChanged, true);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (ParentObject != null)
+ {
+ ParentObject.HitObjectApplied += onHitObjectApplied;
+ onHitObjectApplied(ParentObject);
+
+ ParentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(ParentObject, ParentObject.State.Value);
+ }
+ }
+
+ private void onHitObjectApplied(DrawableHitObject drawableObject)
+ {
+ this.ScaleTo(1f)
+ .FadeOut();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ OnSliderEnd();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (ParentObject != null)
+ {
+ ParentObject.HitObjectApplied -= onHitObjectApplied;
+ ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+
+ protected abstract void OnTrackingChanged(ValueChangedEvent tracking);
+
+ protected abstract void OnSliderEnd();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
index b8a559ce07..5b7da5a1ba 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
@@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Diagnostics;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
- public class LegacyFollowCircle : CompositeDrawable
+ public class LegacyFollowCircle : FollowCircle
{
public LegacyFollowCircle(Drawable animationContent)
{
@@ -18,5 +20,36 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
RelativeSizeAxes = Axes.Both;
InternalChild = animationContent;
}
+
+ protected override void OnTrackingChanged(ValueChangedEvent tracking)
+ {
+ Debug.Assert(ParentObject != null);
+
+ if (ParentObject.Judged)
+ return;
+
+ double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current);
+
+ // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
+ // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
+ if (tracking.NewValue)
+ {
+ // TODO: Follow circle should bounce on each slider tick.
+ this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
+ .FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
+ }
+ else
+ {
+ // TODO: Should animate only at the next slider tick if we want to match stable perfectly.
+ this.ScaleTo(4f, 100)
+ .FadeTo(0f, 100);
+ }
+ }
+
+ protected override void OnSliderEnd()
+ {
+ this.ScaleTo(1.6f, 200, Easing.Out)
+ .FadeOut(200, Easing.In);
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
index 382035119e..d2eba0eb54 100644
--- a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
@@ -11,14 +11,16 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public class BarLine : TaikoHitObject, IBarLine
{
+ private HitObjectProperty major;
+
+ public Bindable MajorBindable => major.Bindable;
+
public bool Major
{
- get => MajorBindable.Value;
- set => MajorBindable.Value = value;
+ get => major.Value;
+ set => major.Value = value;
}
- public readonly Bindable MajorBindable = new BindableBool();
-
public override Judgement CreateJudgement() => new IgnoreJudgement();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs
index 20f3304c30..787079bfee 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs
@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Audio;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
@@ -14,19 +15,21 @@ namespace osu.Game.Rulesets.Taiko.Objects
{
public class Hit : TaikoStrongableHitObject, IHasDisplayColour
{
- public readonly Bindable TypeBindable = new Bindable();
+ private HitObjectProperty type;
- public Bindable DisplayColour { get; } = new Bindable(COLOUR_CENTRE);
+ public Bindable TypeBindable => type.Bindable;
///
/// The that actuates this .
///
public HitType Type
{
- get => TypeBindable.Value;
- set => TypeBindable.Value = value;
+ get => type.Value;
+ set => type.Value = value;
}
+ public Bindable DisplayColour { get; } = new Bindable(COLOUR_CENTRE);
+
public static readonly Color4 COLOUR_CENTRE = Color4Extensions.FromHex(@"bb1177");
public static readonly Color4 COLOUR_RIM = Color4Extensions.FromHex(@"2299bb");
diff --git a/osu.Game.Tests/Extensions/StringDehumanizeExtensionsTest.cs b/osu.Game.Tests/Extensions/StringDehumanizeExtensionsTest.cs
new file mode 100644
index 0000000000..e7490b461b
--- /dev/null
+++ b/osu.Game.Tests/Extensions/StringDehumanizeExtensionsTest.cs
@@ -0,0 +1,85 @@
+// 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.Globalization;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Extensions;
+
+namespace osu.Game.Tests.Extensions
+{
+ [TestFixture]
+ public class StringDehumanizeExtensionsTest
+ {
+ [Test]
+ [TestCase("single", "Single")]
+ [TestCase("example word", "ExampleWord")]
+ [TestCase("mixed Casing test", "MixedCasingTest")]
+ [TestCase("PascalCase", "PascalCase")]
+ [TestCase("camelCase", "CamelCase")]
+ [TestCase("snake_case", "SnakeCase")]
+ [TestCase("kebab-case", "KebabCase")]
+ [TestCase("i will not break in a different culture", "IWillNotBreakInADifferentCulture", "tr-TR")]
+ public void TestToPascalCase(string input, string expectedOutput, string? culture = null)
+ {
+ using (temporaryCurrentCulture(culture))
+ Assert.That(input.ToPascalCase(), Is.EqualTo(expectedOutput));
+ }
+
+ [Test]
+ [TestCase("single", "single")]
+ [TestCase("example word", "exampleWord")]
+ [TestCase("mixed Casing test", "mixedCasingTest")]
+ [TestCase("PascalCase", "pascalCase")]
+ [TestCase("camelCase", "camelCase")]
+ [TestCase("snake_case", "snakeCase")]
+ [TestCase("kebab-case", "kebabCase")]
+ [TestCase("I will not break in a different culture", "iWillNotBreakInADifferentCulture", "tr-TR")]
+ public void TestToCamelCase(string input, string expectedOutput, string? culture = null)
+ {
+ using (temporaryCurrentCulture(culture))
+ Assert.That(input.ToCamelCase(), Is.EqualTo(expectedOutput));
+ }
+
+ [Test]
+ [TestCase("single", "single")]
+ [TestCase("example word", "example_word")]
+ [TestCase("mixed Casing test", "mixed_casing_test")]
+ [TestCase("PascalCase", "pascal_case")]
+ [TestCase("camelCase", "camel_case")]
+ [TestCase("snake_case", "snake_case")]
+ [TestCase("kebab-case", "kebab_case")]
+ [TestCase("I will not break in a different culture", "i_will_not_break_in_a_different_culture", "tr-TR")]
+ public void TestToSnakeCase(string input, string expectedOutput, string? culture = null)
+ {
+ using (temporaryCurrentCulture(culture))
+ Assert.That(input.ToSnakeCase(), Is.EqualTo(expectedOutput));
+ }
+
+ [Test]
+ [TestCase("single", "single")]
+ [TestCase("example word", "example-word")]
+ [TestCase("mixed Casing test", "mixed-casing-test")]
+ [TestCase("PascalCase", "pascal-case")]
+ [TestCase("camelCase", "camel-case")]
+ [TestCase("snake_case", "snake-case")]
+ [TestCase("kebab-case", "kebab-case")]
+ [TestCase("I will not break in a different culture", "i-will-not-break-in-a-different-culture", "tr-TR")]
+ public void TestToKebabCase(string input, string expectedOutput, string? culture = null)
+ {
+ using (temporaryCurrentCulture(culture))
+ Assert.That(input.ToKebabCase(), Is.EqualTo(expectedOutput));
+ }
+
+ private IDisposable temporaryCurrentCulture(string? cultureName)
+ {
+ var storedCulture = CultureInfo.CurrentCulture;
+
+ if (cultureName != null)
+ CultureInfo.CurrentCulture = new CultureInfo(cultureName);
+
+ return new InvokeOnDisposal(() => CultureInfo.CurrentCulture = storedCulture);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
index de23b012c1..461102124a 100644
--- a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
+++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
@@ -4,9 +4,12 @@
#nullable disable
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
+using osu.Game.Models;
+using osu.Game.Tests.Resources;
namespace osu.Game.Tests.NonVisual
{
@@ -23,6 +26,83 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
}
+ [Test]
+ public void TestAudioEqualityNoFile()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ [Test]
+ public void TestAudioEqualitySameHash()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ addAudioFile(beatmapSetA, "abc");
+ addAudioFile(beatmapSetB, "abc");
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ [Test]
+ public void TestAudioEqualityDifferentHash()
+ {
+ var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
+ var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
+
+ addAudioFile(beatmapSetA);
+ addAudioFile(beatmapSetB);
+
+ Assert.AreNotEqual(beatmapSetA, beatmapSetB);
+ Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
+ }
+
+ [Test]
+ public void TestAudioEqualityBeatmapInfoSameHash()
+ {
+ var beatmapSet = TestResources.CreateTestBeatmapSetInfo(2);
+
+ addAudioFile(beatmapSet);
+
+ var beatmap1 = beatmapSet.Beatmaps.First();
+ var beatmap2 = beatmapSet.Beatmaps.Last();
+
+ Assert.AreNotEqual(beatmap1, beatmap2);
+ Assert.IsTrue(beatmap1.AudioEquals(beatmap2));
+ }
+
+ [Test]
+ public void TestAudioEqualityBeatmapInfoDifferentHash()
+ {
+ var beatmapSet = TestResources.CreateTestBeatmapSetInfo(2);
+
+ const string filename1 = "audio1.mp3";
+ const string filename2 = "audio2.mp3";
+
+ addAudioFile(beatmapSet, filename: filename1);
+ addAudioFile(beatmapSet, filename: filename2);
+
+ var beatmap1 = beatmapSet.Beatmaps.First();
+ var beatmap2 = beatmapSet.Beatmaps.Last();
+
+ Assert.AreNotEqual(beatmap1, beatmap2);
+
+ beatmap1.Metadata.AudioFile = filename1;
+ beatmap2.Metadata.AudioFile = filename2;
+
+ Assert.IsFalse(beatmap1.AudioEquals(beatmap2));
+ }
+
+ private static void addAudioFile(BeatmapSetInfo beatmapSetInfo, string hash = null, string filename = null)
+ {
+ beatmapSetInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = hash ?? Guid.NewGuid().ToString() }, filename ?? "audio.mp3"));
+ }
+
[Test]
public void TestDatabasedWithDatabased()
{
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 91d4eb70e8..ee29cc8644 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -134,10 +134,11 @@ namespace osu.Game.Tests.Resources
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
StarRating = diff,
Length = length,
+ BeatmapSet = beatmapSet,
BPM = bpm,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
Ruleset = rulesetInfo,
- Metadata = metadata,
+ Metadata = metadata.DeepClone(),
Difficulty = new BeatmapDifficulty
{
OverallDifficulty = diff,
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
index f565ca3ef4..6ad6f0b299 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Screens;
@@ -103,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
*/
public void TestAddAudioTrack()
{
+ AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
+
AddAssert("switch track to real track", () =>
{
var setup = Editor.ChildrenOfType().First();
@@ -131,6 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
}
});
+ AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNoConflictingModAcronyms.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNoConflictingModAcronyms.cs
new file mode 100644
index 0000000000..b2ba3d99ad
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNoConflictingModAcronyms.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable disable
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneNoConflictingModAcronyms : TestSceneAllRulesetPlayers
+ {
+ protected override void AddCheckSteps()
+ {
+ AddStep("Check all mod acronyms are unique", () =>
+ {
+ var mods = Ruleset.Value.CreateInstance().AllMods;
+
+ IEnumerable acronyms = mods.Select(m => m.Acronym);
+
+ Assert.That(acronyms, Is.Unique);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
index bfc06c0ee0..6491987abe 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
@@ -1,20 +1,25 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Screens;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
@@ -57,13 +62,46 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool HasCustomSteps => true;
- protected override bool AllowFail => false;
+ protected override bool AllowFail => allowFail;
+
+ private bool allowFail;
+
+ [SetUp]
+ public void SetUp()
+ {
+ allowFail = false;
+ customRuleset = null;
+ }
+
+ [Test]
+ public void TestSaveFailedReplay()
+ {
+ AddStep("allow fail", () => allowFail = true);
+
+ CreateTest();
+
+ AddUntilStep("fail screen displayed", () => Player.ChildrenOfType().First().State.Value == Visibility.Visible);
+ AddUntilStep("score not in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) == null));
+ AddStep("click save button", () => Player.ChildrenOfType().First().ChildrenOfType().First().TriggerClick());
+ AddUntilStep("score not in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null));
+ }
+
+ [Test]
+ public void TestLastPlayedUpdated()
+ {
+ DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
+
+ AddAssert("last played is null", () => getLastPlayed() == null);
+
+ CreateTest();
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+ AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
+ }
[Test]
public void TestScoreStoredLocally()
{
- AddStep("set no custom ruleset", () => customRuleset = null);
-
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
index 757dfff2b7..1797c82fb9 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs
@@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Cursor;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@@ -195,12 +196,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestDownloadButtonHiddenWhenBeatmapExists()
{
- var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
Live imported = null;
- Debug.Assert(beatmap.BeatmapSet != null);
+ AddStep("import beatmap", () =>
+ {
+ var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
- AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet));
+ Debug.Assert(beatmap.BeatmapSet != null);
+ imported = manager.Import(beatmap.BeatmapSet);
+ });
createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach()));
@@ -245,40 +249,35 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestExpiredItems()
{
- AddStep("create playlist", () =>
+ createPlaylist(p =>
{
- Child = playlist = new TestPlaylist
+ p.Items.Clear();
+ p.Items.AddRange(new[]
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300),
- Items =
+ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
- new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ ID = 0,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ Expired = true,
+ RequiredMods = new[]
{
- ID = 0,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- Expired = true,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
- },
- new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
+ }
+ },
+ new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
+ {
+ ID = 1,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ RequiredMods = new[]
{
- ID = 1,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
}
}
- };
+ });
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
@@ -321,19 +320,44 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible);
+ private void createPlaylistWithBeatmaps(Func> beatmaps) => createPlaylist(p =>
+ {
+ int index = 0;
+
+ p.Items.Clear();
+
+ foreach (var b in beatmaps())
+ {
+ p.Items.Add(new PlaylistItem(b)
+ {
+ ID = index++,
+ OwnerID = 2,
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
+ RequiredMods = new[]
+ {
+ new APIMod(new OsuModHardRock()),
+ new APIMod(new OsuModDoubleTime()),
+ new APIMod(new OsuModAutoplay())
+ }
+ });
+ }
+ });
+
private void createPlaylist(Action setupPlaylist = null)
{
AddStep("create playlist", () =>
{
- Child = playlist = new TestPlaylist
+ Child = new OsuContextMenuContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300)
+ RelativeSizeAxes = Axes.Both,
+ Child = playlist = new TestPlaylist
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(500, 300)
+ }
};
- setupPlaylist?.Invoke(playlist);
-
for (int i = 0; i < 20; i++)
{
playlist.Items.Add(new PlaylistItem(i % 2 == 1
@@ -360,39 +384,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
- });
- AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
- }
-
- private void createPlaylistWithBeatmaps(Func> beatmaps)
- {
- AddStep("create playlist", () =>
- {
- Child = playlist = new TestPlaylist
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(500, 300)
- };
-
- int index = 0;
-
- foreach (var b in beatmaps())
- {
- playlist.Items.Add(new PlaylistItem(b)
- {
- ID = index++,
- OwnerID = 2,
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
- RequiredMods = new[]
- {
- new APIMod(new OsuModHardRock()),
- new APIMod(new OsuModDoubleTime()),
- new APIMod(new OsuModAutoplay())
- }
- });
- }
+ setupPlaylist?.Invoke(playlist);
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index a70dfd78c5..edd1491865 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
@@ -368,12 +369,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
ParticipantsList? participantsList = null;
- AddStep("create new list", () => Child = participantsList = new ParticipantsList
+ AddStep("create new list", () => Child = new OsuContextMenuContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Y,
- Size = new Vector2(380, 0.7f)
+ RelativeSizeAxes = Axes.Both,
+ Child = participantsList = new ParticipantsList
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ Size = new Vector2(380, 0.7f)
+ }
});
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
index c5c61cdd72..5454c87dff 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3103765,
IsOnline = true,
Statistics = new UserStatistics { GlobalRank = 1111 },
- Country = new Country { FlagName = "JP" },
+ CountryCode = CountryCode.JP,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
},
new APIUser
@@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 2,
IsOnline = false,
Statistics = new UserStatistics { GlobalRank = 2222 },
- Country = new Country { FlagName = "AU" },
+ CountryCode = CountryCode.AU,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
IsSupporter = true,
SupportLevel = 3,
@@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = "Evast",
Id = 8195163,
- Country = new Country { FlagName = "BY" },
+ CountryCode = CountryCode.BY,
CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false,
LastVisit = DateTimeOffset.Now
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs
index e7d799222a..6a39db4870 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsCountryFilter.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneRankingsCountryFilter()
{
- var countryBindable = new Bindable();
+ var countryBindable = new Bindable();
AddRange(new Drawable[]
{
@@ -56,20 +56,12 @@ namespace osu.Game.Tests.Visual.Online
}
});
- var country = new Country
- {
- FlagName = "BY",
- FullName = "Belarus"
- };
- var unknownCountry = new Country
- {
- FlagName = "CK",
- FullName = "Cook Islands"
- };
+ const CountryCode country = CountryCode.BY;
+ const CountryCode unknown_country = CountryCode.CK;
AddStep("Set country", () => countryBindable.Value = country);
- AddStep("Set null country", () => countryBindable.Value = null);
- AddStep("Set country with no flag", () => countryBindable.Value = unknownCountry);
+ AddStep("Set default country", () => countryBindable.Value = default);
+ AddStep("Set country with no flag", () => countryBindable.Value = unknown_country);
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs
index c8f08d70be..c776cfe377 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsHeader.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneRankingsHeader()
{
- var countryBindable = new Bindable();
+ var countryBindable = new Bindable();
var ruleset = new Bindable();
var scope = new Bindable();
@@ -30,21 +30,12 @@ namespace osu.Game.Tests.Visual.Online
Ruleset = { BindTarget = ruleset }
});
- var country = new Country
- {
- FlagName = "BY",
- FullName = "Belarus"
- };
-
- var unknownCountry = new Country
- {
- FlagName = "CK",
- FullName = "Cook Islands"
- };
+ const CountryCode country = CountryCode.BY;
+ const CountryCode unknown_country = CountryCode.CK;
AddStep("Set country", () => countryBindable.Value = country);
AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score);
- AddStep("Set country with no flag", () => countryBindable.Value = unknownCountry);
+ AddStep("Set country with no flag", () => countryBindable.Value = unknown_country);
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
index 62dad7b458..5476049882 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Online
private TestRankingsOverlay rankingsOverlay;
- private readonly Bindable countryBindable = new Bindable();
+ private readonly Bindable countryBindable = new Bindable();
private readonly Bindable scope = new Bindable();
[SetUp]
@@ -48,15 +48,15 @@ namespace osu.Game.Tests.Visual.Online
public void TestFlagScopeDependency()
{
AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score);
- AddAssert("Check country is Null", () => countryBindable.Value == null);
- AddStep("Set country", () => countryBindable.Value = us_country);
+ AddAssert("Check country is default", () => countryBindable.IsDefault);
+ AddStep("Set country", () => countryBindable.Value = CountryCode.US);
AddAssert("Check scope is Performance", () => scope.Value == RankingsScope.Performance);
}
[Test]
public void TestShowCountry()
{
- AddStep("Show US", () => rankingsOverlay.ShowCountry(us_country));
+ AddStep("Show US", () => rankingsOverlay.ShowCountry(CountryCode.US));
}
private void loadRankingsOverlay()
@@ -69,15 +69,9 @@ namespace osu.Game.Tests.Visual.Online
};
}
- private static readonly Country us_country = new Country
- {
- FlagName = "US",
- FullName = "United States"
- };
-
private class TestRankingsOverlay : RankingsOverlay
{
- public new Bindable Country => base.Country;
+ public new Bindable Country => base.Country;
}
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
index e357b0fffc..81b76d19ac 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
@@ -57,8 +57,7 @@ namespace osu.Game.Tests.Visual.Online
{
new CountryStatistics
{
- Country = new Country { FlagName = "US", FullName = "United States" },
- FlagName = "US",
+ Code = CountryCode.US,
ActiveUsers = 2_972_623,
PlayCount = 3_086_515_743,
RankedScore = 449_407_643_332_546,
@@ -66,8 +65,7 @@ namespace osu.Game.Tests.Visual.Online
},
new CountryStatistics
{
- Country = new Country { FlagName = "RU", FullName = "Russian Federation" },
- FlagName = "RU",
+ Code = CountryCode.RU,
ActiveUsers = 1_609_989,
PlayCount = 1_637_052_841,
RankedScore = 221_660_827_473_004,
@@ -86,7 +84,7 @@ namespace osu.Game.Tests.Visual.Online
User = new APIUser
{
Username = "first active user",
- Country = new Country { FlagName = "JP" },
+ CountryCode = CountryCode.JP,
Active = true,
},
Accuracy = 0.9972,
@@ -106,7 +104,7 @@ namespace osu.Game.Tests.Visual.Online
User = new APIUser
{
Username = "inactive user",
- Country = new Country { FlagName = "AU" },
+ CountryCode = CountryCode.AU,
Active = false,
},
Accuracy = 0.9831,
@@ -126,7 +124,7 @@ namespace osu.Game.Tests.Visual.Online
User = new APIUser
{
Username = "second active user",
- Country = new Country { FlagName = "PL" },
+ CountryCode = CountryCode.PL,
Active = true,
},
Accuracy = 0.9584,
diff --git a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
index 16a34e996f..beca3a8700 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
@@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Users;
using osuTK.Graphics;
@@ -146,21 +147,17 @@ namespace osu.Game.Tests.Visual.Online
{
var scores = new APIScoresCollection
{
- Scores = new List
+ Scores = new List
{
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 6602580,
Username = @"waaiiru",
- Country = new Country
- {
- FullName = @"Spain",
- FlagName = @"ES",
- },
+ CountryCode = CountryCode.ES,
},
Mods = new[]
{
@@ -175,19 +172,15 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890,
Accuracy = 1,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 4608074,
Username = @"Skycries",
- Country = new Country
- {
- FullName = @"Brazil",
- FlagName = @"BR",
- },
+ CountryCode = CountryCode.BR,
},
Mods = new[]
{
@@ -201,19 +194,15 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789,
Accuracy = 0.9997,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 1014222,
Username = @"eLy",
- Country = new Country
- {
- FullName = @"Japan",
- FlagName = @"JP",
- },
+ CountryCode = CountryCode.JP,
},
Mods = new[]
{
@@ -226,19 +215,15 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678,
Accuracy = 0.9854,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 1541390,
Username = @"Toukai",
- Country = new Country
- {
- FullName = @"Canada",
- FlagName = @"CA",
- },
+ CountryCode = CountryCode.CA,
},
Mods = new[]
{
@@ -250,19 +235,15 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567,
Accuracy = 0.8765,
},
- new APIScore
+ new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 7151382,
Username = @"Mayuri Hana",
- Country = new Country
- {
- FullName = @"Thailand",
- FlagName = @"TH",
- },
+ CountryCode = CountryCode.TH,
},
Rank = ScoreRank.D,
PP = 160,
@@ -273,14 +254,18 @@ namespace osu.Game.Tests.Visual.Online
}
};
+ const int initial_great_count = 2000;
+
+ int greatCount = initial_great_count;
+
foreach (var s in scores.Scores)
{
- s.Statistics = new Dictionary
+ s.Statistics = new Dictionary
{
- { "count_300", RNG.Next(2000) },
- { "count_100", RNG.Next(2000) },
- { "count_50", RNG.Next(2000) },
- { "count_miss", RNG.Next(2000) }
+ { HitResult.Great, greatCount -= 100 },
+ { HitResult.Ok, RNG.Next(100) },
+ { HitResult.Meh, RNG.Next(100) },
+ { HitResult.Miss, initial_great_count - greatCount }
};
}
@@ -289,19 +274,15 @@ namespace osu.Game.Tests.Visual.Online
private APIScoreWithPosition createUserBest() => new APIScoreWithPosition
{
- Score = new APIScore
+ Score = new SoloScoreInfo
{
- Date = DateTimeOffset.Now,
- OnlineID = onlineID++,
+ EndedAt = DateTimeOffset.Now,
+ ID = onlineID++,
User = new APIUser
{
Id = 7151382,
Username = @"Mayuri Hana",
- Country = new Country
- {
- FullName = @"Thailand",
- FlagName = @"TH",
- },
+ CountryCode = CountryCode.TH,
},
Rank = ScoreRank.D,
PP = 160,
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
index fff40b3c74..2a70fd7df3 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = @"flyte",
Id = 3103765,
- Country = new Country { FlagName = @"JP" },
+ CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
Status = { Value = new UserStatusOnline() }
}) { Width = 300 },
@@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = @"peppy",
Id = 2,
- Country = new Country { FlagName = @"AU" },
+ CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
IsSupporter = true,
SupportLevel = 3,
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = @"Evast",
Id = 8195163,
- Country = new Country { FlagName = @"BY" },
+ CountryCode = CountryCode.BY,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false,
LastVisit = DateTimeOffset.Now
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index ad3215b1ef..caa2d2571d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = @"Somebody",
Id = 1,
- Country = new Country { FullName = @"Alien" },
+ CountryCode = CountryCode.Unknown,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
JoinDate = DateTimeOffset.Now.AddDays(-1),
LastVisit = DateTimeOffset.Now,
@@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"peppy",
Id = 2,
IsSupporter = true,
- Country = new Country { FullName = @"Australia", FlagName = @"AU" },
+ CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg"
}));
@@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = @"flyte",
Id = 3103765,
- Country = new Country { FullName = @"Japan", FlagName = @"JP" },
+ CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
}));
@@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"BanchoBot",
Id = 3,
IsBot = true,
- Country = new Country { FullName = @"Saint Helena", FlagName = @"SH" },
+ CountryCode = CountryCode.SH,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg"
}));
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
index fa28df3061..0eb6ec3c04 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Online
{
public TestSceneUserProfileScores()
{
- var firstScore = new APIScore
+ var firstScore = new SoloScoreInfo
{
PP = 1047.21,
Rank = ScoreRank.SH,
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Online
},
DifficultyName = "Extreme"
},
- Date = DateTimeOffset.Now,
+ EndedAt = DateTimeOffset.Now,
Mods = new[]
{
new APIMod { Acronym = new OsuModHidden().Acronym },
@@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.9813
};
- var secondScore = new APIScore
+ var secondScore = new SoloScoreInfo
{
PP = 134.32,
Rank = ScoreRank.A,
@@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Online
},
DifficultyName = "[4K] Regret"
},
- Date = DateTimeOffset.Now,
+ EndedAt = DateTimeOffset.Now,
Mods = new[]
{
new APIMod { Acronym = new OsuModHardRock().Acronym },
@@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.998546
};
- var thirdScore = new APIScore
+ var thirdScore = new SoloScoreInfo
{
PP = 96.83,
Rank = ScoreRank.S,
@@ -79,11 +79,11 @@ namespace osu.Game.Tests.Visual.Online
},
DifficultyName = "Insane"
},
- Date = DateTimeOffset.Now,
+ EndedAt = DateTimeOffset.Now,
Accuracy = 0.9726
};
- var noPPScore = new APIScore
+ var noPPScore = new SoloScoreInfo
{
Rank = ScoreRank.B,
Beatmap = new APIBeatmap
@@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online
},
DifficultyName = "[4K] Cataclysmic Hypernova"
},
- Date = DateTimeOffset.Now,
+ EndedAt = DateTimeOffset.Now,
Accuracy = 0.55879
};
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index ef0c7d7d4d..abcb888cd4 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -140,11 +140,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 6602580,
Username = @"waaiiru",
- Country = new Country
- {
- FullName = @"Spain",
- FlagName = @"ES",
- },
+ CountryCode = CountryCode.ES,
},
});
}
@@ -164,12 +160,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 6602580,
Username = @"waaiiru",
- Country = new Country
- {
- FullName = @"Spain",
- FlagName = @"ES",
- },
- },
+ CountryCode = CountryCode.ES,
+ }
});
}
@@ -225,11 +217,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 6602580,
Username = @"waaiiru",
- Country = new Country
- {
- FullName = @"Spain",
- FlagName = @"ES",
- },
+ CountryCode = CountryCode.ES,
},
},
new ScoreInfo
@@ -246,11 +234,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 4608074,
Username = @"Skycries",
- Country = new Country
- {
- FullName = @"Brazil",
- FlagName = @"BR",
- },
+ CountryCode = CountryCode.BR,
},
},
new ScoreInfo
@@ -268,11 +252,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 1014222,
Username = @"eLy",
- Country = new Country
- {
- FullName = @"Japan",
- FlagName = @"JP",
- },
+ CountryCode = CountryCode.JP,
},
},
new ScoreInfo
@@ -290,11 +270,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 1541390,
Username = @"Toukai",
- Country = new Country
- {
- FullName = @"Canada",
- FlagName = @"CA",
- },
+ CountryCode = CountryCode.CA,
},
},
new ScoreInfo
@@ -312,11 +288,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 2243452,
Username = @"Satoruu",
- Country = new Country
- {
- FullName = @"Venezuela",
- FlagName = @"VE",
- },
+ CountryCode = CountryCode.VE,
},
},
new ScoreInfo
@@ -334,11 +306,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 2705430,
Username = @"Mooha",
- Country = new Country
- {
- FullName = @"France",
- FlagName = @"FR",
- },
+ CountryCode = CountryCode.FR,
},
},
new ScoreInfo
@@ -356,11 +324,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 7151382,
Username = @"Mayuri Hana",
- Country = new Country
- {
- FullName = @"Thailand",
- FlagName = @"TH",
- },
+ CountryCode = CountryCode.TH,
},
},
new ScoreInfo
@@ -378,11 +342,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 2051389,
Username = @"FunOrange",
- Country = new Country
- {
- FullName = @"Canada",
- FlagName = @"CA",
- },
+ CountryCode = CountryCode.CA,
},
},
new ScoreInfo
@@ -400,11 +360,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 6169483,
Username = @"-Hebel-",
- Country = new Country
- {
- FullName = @"Mexico",
- FlagName = @"MX",
- },
+ CountryCode = CountryCode.MX,
},
},
new ScoreInfo
@@ -422,11 +378,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 6702666,
Username = @"prhtnsm",
- Country = new Country
- {
- FullName = @"Germany",
- FlagName = @"DE",
- },
+ CountryCode = CountryCode.DE,
},
},
};
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs
index 16966e489a..39fd9fda2b 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs
@@ -69,11 +69,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 6602580,
Username = @"waaiiru",
- Country = new Country
- {
- FullName = @"Spain",
- FlagName = @"ES",
- },
+ CountryCode = CountryCode.ES,
},
},
new ScoreInfo
@@ -88,11 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 4608074,
Username = @"Skycries",
- Country = new Country
- {
- FullName = @"Brazil",
- FlagName = @"BR",
- },
+ CountryCode = CountryCode.BR,
},
},
new ScoreInfo
@@ -107,11 +99,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Id = 1541390,
Username = @"Toukai",
- Country = new Country
- {
- FullName = @"Canada",
- FlagName = @"CA",
- },
+ CountryCode = CountryCode.CA,
},
}
};
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
index 44f2da2b95..e8454e8d0f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
@@ -4,13 +4,13 @@
#nullable disable
using System.Linq;
-using Humanizer;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.UserInterface
};
control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
- control.General.BindCollectionChanged((_, _) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().Underscore())) : "")}", true);
+ control.General.BindCollectionChanged((_, _) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().ToSnakeCase())) : "")}", true);
control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);
diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
index 0fc3646585..b088670caa 100644
--- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
+++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs
@@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Components
{
public class TournamentSpriteTextWithBackground : CompositeDrawable
{
- protected readonly TournamentSpriteText Text;
+ public readonly TournamentSpriteText Text;
+
protected readonly Box Background;
public TournamentSpriteTextWithBackground(string text = "")
diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs
index c6bbb54f9a..2e79998e66 100644
--- a/osu.Game.Tournament/Components/TourneyVideo.cs
+++ b/osu.Game.Tournament/Components/TourneyVideo.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Components
private Video video;
private ManualClock manualClock;
+ public bool VideoAvailable => video != null;
+
public TourneyVideo(string filename, bool drawFallbackGradient = false)
{
this.filename = filename;
diff --git a/osu.Game.Tournament/CountryExtensions.cs b/osu.Game.Tournament/CountryExtensions.cs
new file mode 100644
index 0000000000..f2a583c8a5
--- /dev/null
+++ b/osu.Game.Tournament/CountryExtensions.cs
@@ -0,0 +1,770 @@
+// 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.Users;
+
+namespace osu.Game.Tournament
+{
+ public static class CountryExtensions
+ {
+ public static string GetAcronym(this CountryCode country)
+ {
+ switch (country)
+ {
+ case CountryCode.BD:
+ return "BGD";
+
+ case CountryCode.BE:
+ return "BEL";
+
+ case CountryCode.BF:
+ return "BFA";
+
+ case CountryCode.BG:
+ return "BGR";
+
+ case CountryCode.BA:
+ return "BIH";
+
+ case CountryCode.BB:
+ return "BRB";
+
+ case CountryCode.WF:
+ return "WLF";
+
+ case CountryCode.BL:
+ return "BLM";
+
+ case CountryCode.BM:
+ return "BMU";
+
+ case CountryCode.BN:
+ return "BRN";
+
+ case CountryCode.BO:
+ return "BOL";
+
+ case CountryCode.BH:
+ return "BHR";
+
+ case CountryCode.BI:
+ return "BDI";
+
+ case CountryCode.BJ:
+ return "BEN";
+
+ case CountryCode.BT:
+ return "BTN";
+
+ case CountryCode.JM:
+ return "JAM";
+
+ case CountryCode.BV:
+ return "BVT";
+
+ case CountryCode.BW:
+ return "BWA";
+
+ case CountryCode.WS:
+ return "WSM";
+
+ case CountryCode.BQ:
+ return "BES";
+
+ case CountryCode.BR:
+ return "BRA";
+
+ case CountryCode.BS:
+ return "BHS";
+
+ case CountryCode.JE:
+ return "JEY";
+
+ case CountryCode.BY:
+ return "BLR";
+
+ case CountryCode.BZ:
+ return "BLZ";
+
+ case CountryCode.RU:
+ return "RUS";
+
+ case CountryCode.RW:
+ return "RWA";
+
+ case CountryCode.RS:
+ return "SRB";
+
+ case CountryCode.TL:
+ return "TLS";
+
+ case CountryCode.RE:
+ return "REU";
+
+ case CountryCode.TM:
+ return "TKM";
+
+ case CountryCode.TJ:
+ return "TJK";
+
+ case CountryCode.RO:
+ return "ROU";
+
+ case CountryCode.TK:
+ return "TKL";
+
+ case CountryCode.GW:
+ return "GNB";
+
+ case CountryCode.GU:
+ return "GUM";
+
+ case CountryCode.GT:
+ return "GTM";
+
+ case CountryCode.GS:
+ return "SGS";
+
+ case CountryCode.GR:
+ return "GRC";
+
+ case CountryCode.GQ:
+ return "GNQ";
+
+ case CountryCode.GP:
+ return "GLP";
+
+ case CountryCode.JP:
+ return "JPN";
+
+ case CountryCode.GY:
+ return "GUY";
+
+ case CountryCode.GG:
+ return "GGY";
+
+ case CountryCode.GF:
+ return "GUF";
+
+ case CountryCode.GE:
+ return "GEO";
+
+ case CountryCode.GD:
+ return "GRD";
+
+ case CountryCode.GB:
+ return "GBR";
+
+ case CountryCode.GA:
+ return "GAB";
+
+ case CountryCode.SV:
+ return "SLV";
+
+ case CountryCode.GN:
+ return "GIN";
+
+ case CountryCode.GM:
+ return "GMB";
+
+ case CountryCode.GL:
+ return "GRL";
+
+ case CountryCode.GI:
+ return "GIB";
+
+ case CountryCode.GH:
+ return "GHA";
+
+ case CountryCode.OM:
+ return "OMN";
+
+ case CountryCode.TN:
+ return "TUN";
+
+ case CountryCode.JO:
+ return "JOR";
+
+ case CountryCode.HR:
+ return "HRV";
+
+ case CountryCode.HT:
+ return "HTI";
+
+ case CountryCode.HU:
+ return "HUN";
+
+ case CountryCode.HK:
+ return "HKG";
+
+ case CountryCode.HN:
+ return "HND";
+
+ case CountryCode.HM:
+ return "HMD";
+
+ case CountryCode.VE:
+ return "VEN";
+
+ case CountryCode.PR:
+ return "PRI";
+
+ case CountryCode.PS:
+ return "PSE";
+
+ case CountryCode.PW:
+ return "PLW";
+
+ case CountryCode.PT:
+ return "PRT";
+
+ case CountryCode.SJ:
+ return "SJM";
+
+ case CountryCode.PY:
+ return "PRY";
+
+ case CountryCode.IQ:
+ return "IRQ";
+
+ case CountryCode.PA:
+ return "PAN";
+
+ case CountryCode.PF:
+ return "PYF";
+
+ case CountryCode.PG:
+ return "PNG";
+
+ case CountryCode.PE:
+ return "PER";
+
+ case CountryCode.PK:
+ return "PAK";
+
+ case CountryCode.PH:
+ return "PHL";
+
+ case CountryCode.PN:
+ return "PCN";
+
+ case CountryCode.PL:
+ return "POL";
+
+ case CountryCode.PM:
+ return "SPM";
+
+ case CountryCode.ZM:
+ return "ZMB";
+
+ case CountryCode.EH:
+ return "ESH";
+
+ case CountryCode.EE:
+ return "EST";
+
+ case CountryCode.EG:
+ return "EGY";
+
+ case CountryCode.ZA:
+ return "ZAF";
+
+ case CountryCode.EC:
+ return "ECU";
+
+ case CountryCode.IT:
+ return "ITA";
+
+ case CountryCode.VN:
+ return "VNM";
+
+ case CountryCode.SB:
+ return "SLB";
+
+ case CountryCode.ET:
+ return "ETH";
+
+ case CountryCode.SO:
+ return "SOM";
+
+ case CountryCode.ZW:
+ return "ZWE";
+
+ case CountryCode.SA:
+ return "SAU";
+
+ case CountryCode.ES:
+ return "ESP";
+
+ case CountryCode.ER:
+ return "ERI";
+
+ case CountryCode.ME:
+ return "MNE";
+
+ case CountryCode.MD:
+ return "MDA";
+
+ case CountryCode.MG:
+ return "MDG";
+
+ case CountryCode.MF:
+ return "MAF";
+
+ case CountryCode.MA:
+ return "MAR";
+
+ case CountryCode.MC:
+ return "MCO";
+
+ case CountryCode.UZ:
+ return "UZB";
+
+ case CountryCode.MM:
+ return "MMR";
+
+ case CountryCode.ML:
+ return "MLI";
+
+ case CountryCode.MO:
+ return "MAC";
+
+ case CountryCode.MN:
+ return "MNG";
+
+ case CountryCode.MH:
+ return "MHL";
+
+ case CountryCode.MK:
+ return "MKD";
+
+ case CountryCode.MU:
+ return "MUS";
+
+ case CountryCode.MT:
+ return "MLT";
+
+ case CountryCode.MW:
+ return "MWI";
+
+ case CountryCode.MV:
+ return "MDV";
+
+ case CountryCode.MQ:
+ return "MTQ";
+
+ case CountryCode.MP:
+ return "MNP";
+
+ case CountryCode.MS:
+ return "MSR";
+
+ case CountryCode.MR:
+ return "MRT";
+
+ case CountryCode.IM:
+ return "IMN";
+
+ case CountryCode.UG:
+ return "UGA";
+
+ case CountryCode.TZ:
+ return "TZA";
+
+ case CountryCode.MY:
+ return "MYS";
+
+ case CountryCode.MX:
+ return "MEX";
+
+ case CountryCode.IL:
+ return "ISR";
+
+ case CountryCode.FR:
+ return "FRA";
+
+ case CountryCode.IO:
+ return "IOT";
+
+ case CountryCode.SH:
+ return "SHN";
+
+ case CountryCode.FI:
+ return "FIN";
+
+ case CountryCode.FJ:
+ return "FJI";
+
+ case CountryCode.FK:
+ return "FLK";
+
+ case CountryCode.FM:
+ return "FSM";
+
+ case CountryCode.FO:
+ return "FRO";
+
+ case CountryCode.NI:
+ return "NIC";
+
+ case CountryCode.NL:
+ return "NLD";
+
+ case CountryCode.NO:
+ return "NOR";
+
+ case CountryCode.NA:
+ return "NAM";
+
+ case CountryCode.VU:
+ return "VUT";
+
+ case CountryCode.NC:
+ return "NCL";
+
+ case CountryCode.NE:
+ return "NER";
+
+ case CountryCode.NF:
+ return "NFK";
+
+ case CountryCode.NG:
+ return "NGA";
+
+ case CountryCode.NZ:
+ return "NZL";
+
+ case CountryCode.NP:
+ return "NPL";
+
+ case CountryCode.NR:
+ return "NRU";
+
+ case CountryCode.NU:
+ return "NIU";
+
+ case CountryCode.CK:
+ return "COK";
+
+ case CountryCode.XK:
+ return "XKX";
+
+ case CountryCode.CI:
+ return "CIV";
+
+ case CountryCode.CH:
+ return "CHE";
+
+ case CountryCode.CO:
+ return "COL";
+
+ case CountryCode.CN:
+ return "CHN";
+
+ case CountryCode.CM:
+ return "CMR";
+
+ case CountryCode.CL:
+ return "CHL";
+
+ case CountryCode.CC:
+ return "CCK";
+
+ case CountryCode.CA:
+ return "CAN";
+
+ case CountryCode.CG:
+ return "COG";
+
+ case CountryCode.CF:
+ return "CAF";
+
+ case CountryCode.CD:
+ return "COD";
+
+ case CountryCode.CZ:
+ return "CZE";
+
+ case CountryCode.CY:
+ return "CYP";
+
+ case CountryCode.CX:
+ return "CXR";
+
+ case CountryCode.CR:
+ return "CRI";
+
+ case CountryCode.CW:
+ return "CUW";
+
+ case CountryCode.CV:
+ return "CPV";
+
+ case CountryCode.CU:
+ return "CUB";
+
+ case CountryCode.SZ:
+ return "SWZ";
+
+ case CountryCode.SY:
+ return "SYR";
+
+ case CountryCode.SX:
+ return "SXM";
+
+ case CountryCode.KG:
+ return "KGZ";
+
+ case CountryCode.KE:
+ return "KEN";
+
+ case CountryCode.SS:
+ return "SSD";
+
+ case CountryCode.SR:
+ return "SUR";
+
+ case CountryCode.KI:
+ return "KIR";
+
+ case CountryCode.KH:
+ return "KHM";
+
+ case CountryCode.KN:
+ return "KNA";
+
+ case CountryCode.KM:
+ return "COM";
+
+ case CountryCode.ST:
+ return "STP";
+
+ case CountryCode.SK:
+ return "SVK";
+
+ case CountryCode.KR:
+ return "KOR";
+
+ case CountryCode.SI:
+ return "SVN";
+
+ case CountryCode.KP:
+ return "PRK";
+
+ case CountryCode.KW:
+ return "KWT";
+
+ case CountryCode.SN:
+ return "SEN";
+
+ case CountryCode.SM:
+ return "SMR";
+
+ case CountryCode.SL:
+ return "SLE";
+
+ case CountryCode.SC:
+ return "SYC";
+
+ case CountryCode.KZ:
+ return "KAZ";
+
+ case CountryCode.KY:
+ return "CYM";
+
+ case CountryCode.SG:
+ return "SGP";
+
+ case CountryCode.SE:
+ return "SWE";
+
+ case CountryCode.SD:
+ return "SDN";
+
+ case CountryCode.DO:
+ return "DOM";
+
+ case CountryCode.DM:
+ return "DMA";
+
+ case CountryCode.DJ:
+ return "DJI";
+
+ case CountryCode.DK:
+ return "DNK";
+
+ case CountryCode.VG:
+ return "VGB";
+
+ case CountryCode.DE:
+ return "DEU";
+
+ case CountryCode.YE:
+ return "YEM";
+
+ case CountryCode.DZ:
+ return "DZA";
+
+ case CountryCode.US:
+ return "USA";
+
+ case CountryCode.UY:
+ return "URY";
+
+ case CountryCode.YT:
+ return "MYT";
+
+ case CountryCode.UM:
+ return "UMI";
+
+ case CountryCode.LB:
+ return "LBN";
+
+ case CountryCode.LC:
+ return "LCA";
+
+ case CountryCode.LA:
+ return "LAO";
+
+ case CountryCode.TV:
+ return "TUV";
+
+ case CountryCode.TW:
+ return "TWN";
+
+ case CountryCode.TT:
+ return "TTO";
+
+ case CountryCode.TR:
+ return "TUR";
+
+ case CountryCode.LK:
+ return "LKA";
+
+ case CountryCode.LI:
+ return "LIE";
+
+ case CountryCode.LV:
+ return "LVA";
+
+ case CountryCode.TO:
+ return "TON";
+
+ case CountryCode.LT:
+ return "LTU";
+
+ case CountryCode.LU:
+ return "LUX";
+
+ case CountryCode.LR:
+ return "LBR";
+
+ case CountryCode.LS:
+ return "LSO";
+
+ case CountryCode.TH:
+ return "THA";
+
+ case CountryCode.TF:
+ return "ATF";
+
+ case CountryCode.TG:
+ return "TGO";
+
+ case CountryCode.TD:
+ return "TCD";
+
+ case CountryCode.TC:
+ return "TCA";
+
+ case CountryCode.LY:
+ return "LBY";
+
+ case CountryCode.VA:
+ return "VAT";
+
+ case CountryCode.VC:
+ return "VCT";
+
+ case CountryCode.AE:
+ return "ARE";
+
+ case CountryCode.AD:
+ return "AND";
+
+ case CountryCode.AG:
+ return "ATG";
+
+ case CountryCode.AF:
+ return "AFG";
+
+ case CountryCode.AI:
+ return "AIA";
+
+ case CountryCode.VI:
+ return "VIR";
+
+ case CountryCode.IS:
+ return "ISL";
+
+ case CountryCode.IR:
+ return "IRN";
+
+ case CountryCode.AM:
+ return "ARM";
+
+ case CountryCode.AL:
+ return "ALB";
+
+ case CountryCode.AO:
+ return "AGO";
+
+ case CountryCode.AQ:
+ return "ATA";
+
+ case CountryCode.AS:
+ return "ASM";
+
+ case CountryCode.AR:
+ return "ARG";
+
+ case CountryCode.AU:
+ return "AUS";
+
+ case CountryCode.AT:
+ return "AUT";
+
+ case CountryCode.AW:
+ return "ABW";
+
+ case CountryCode.IN:
+ return "IND";
+
+ case CountryCode.AX:
+ return "ALA";
+
+ case CountryCode.AZ:
+ return "AZE";
+
+ case CountryCode.IE:
+ return "IRL";
+
+ case CountryCode.ID:
+ return "IDN";
+
+ case CountryCode.UA:
+ return "UKR";
+
+ case CountryCode.QA:
+ return "QAT";
+
+ case CountryCode.MZ:
+ return "MOZ";
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(country));
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Models/SeedingBeatmap.cs b/osu.Game.Tournament/Models/SeedingBeatmap.cs
index 03beb7ca9a..fb0e20556c 100644
--- a/osu.Game.Tournament/Models/SeedingBeatmap.cs
+++ b/osu.Game.Tournament/Models/SeedingBeatmap.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using Newtonsoft.Json;
using osu.Framework.Bindables;
@@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Models
public int ID;
[JsonProperty("BeatmapInfo")]
- public TournamentBeatmap Beatmap;
+ public TournamentBeatmap? Beatmap;
public long Score;
diff --git a/osu.Game.Tournament/Models/TournamentUser.cs b/osu.Game.Tournament/Models/TournamentUser.cs
index 80e58538e5..78ca014860 100644
--- a/osu.Game.Tournament/Models/TournamentUser.cs
+++ b/osu.Game.Tournament/Models/TournamentUser.cs
@@ -22,7 +22,8 @@ namespace osu.Game.Tournament.Models
///
/// The player's country.
///
- public Country? Country { get; set; }
+ [JsonProperty("country_code")]
+ public CountryCode CountryCode { get; set; }
///
/// The player's global rank, or null if not available.
@@ -40,7 +41,7 @@ namespace osu.Game.Tournament.Models
{
Id = OnlineID,
Username = Username,
- Country = Country,
+ CountryCode = CountryCode,
CoverUrl = CoverUrl,
};
diff --git a/osu.Game.Tournament/Resources/countries.json b/osu.Game.Tournament/Resources/countries.json
deleted file mode 100644
index 7306a8bec5..0000000000
--- a/osu.Game.Tournament/Resources/countries.json
+++ /dev/null
@@ -1,1252 +0,0 @@
-[
- {
- "FlagName": "BD",
- "FullName": "Bangladesh",
- "Acronym": "BGD"
- },
- {
- "FlagName": "BE",
- "FullName": "Belgium",
- "Acronym": "BEL"
- },
- {
- "FlagName": "BF",
- "FullName": "Burkina Faso",
- "Acronym": "BFA"
- },
- {
- "FlagName": "BG",
- "FullName": "Bulgaria",
- "Acronym": "BGR"
- },
- {
- "FlagName": "BA",
- "FullName": "Bosnia and Herzegovina",
- "Acronym": "BIH"
- },
- {
- "FlagName": "BB",
- "FullName": "Barbados",
- "Acronym": "BRB"
- },
- {
- "FlagName": "WF",
- "FullName": "Wallis and Futuna",
- "Acronym": "WLF"
- },
- {
- "FlagName": "BL",
- "FullName": "Saint Barthelemy",
- "Acronym": "BLM"
- },
- {
- "FlagName": "BM",
- "FullName": "Bermuda",
- "Acronym": "BMU"
- },
- {
- "FlagName": "BN",
- "FullName": "Brunei",
- "Acronym": "BRN"
- },
- {
- "FlagName": "BO",
- "FullName": "Bolivia",
- "Acronym": "BOL"
- },
- {
- "FlagName": "BH",
- "FullName": "Bahrain",
- "Acronym": "BHR"
- },
- {
- "FlagName": "BI",
- "FullName": "Burundi",
- "Acronym": "BDI"
- },
- {
- "FlagName": "BJ",
- "FullName": "Benin",
- "Acronym": "BEN"
- },
- {
- "FlagName": "BT",
- "FullName": "Bhutan",
- "Acronym": "BTN"
- },
- {
- "FlagName": "JM",
- "FullName": "Jamaica",
- "Acronym": "JAM"
- },
- {
- "FlagName": "BV",
- "FullName": "Bouvet Island",
- "Acronym": "BVT"
- },
- {
- "FlagName": "BW",
- "FullName": "Botswana",
- "Acronym": "BWA"
- },
- {
- "FlagName": "WS",
- "FullName": "Samoa",
- "Acronym": "WSM"
- },
- {
- "FlagName": "BQ",
- "FullName": "Bonaire, Saint Eustatius and Saba",
- "Acronym": "BES"
- },
- {
- "FlagName": "BR",
- "FullName": "Brazil",
- "Acronym": "BRA"
- },
- {
- "FlagName": "BS",
- "FullName": "Bahamas",
- "Acronym": "BHS"
- },
- {
- "FlagName": "JE",
- "FullName": "Jersey",
- "Acronym": "JEY"
- },
- {
- "FlagName": "BY",
- "FullName": "Belarus",
- "Acronym": "BLR"
- },
- {
- "FlagName": "BZ",
- "FullName": "Belize",
- "Acronym": "BLZ"
- },
- {
- "FlagName": "RU",
- "FullName": "Russia",
- "Acronym": "RUS"
- },
- {
- "FlagName": "RW",
- "FullName": "Rwanda",
- "Acronym": "RWA"
- },
- {
- "FlagName": "RS",
- "FullName": "Serbia",
- "Acronym": "SRB"
- },
- {
- "FlagName": "TL",
- "FullName": "East Timor",
- "Acronym": "TLS"
- },
- {
- "FlagName": "RE",
- "FullName": "Reunion",
- "Acronym": "REU"
- },
- {
- "FlagName": "TM",
- "FullName": "Turkmenistan",
- "Acronym": "TKM"
- },
- {
- "FlagName": "TJ",
- "FullName": "Tajikistan",
- "Acronym": "TJK"
- },
- {
- "FlagName": "RO",
- "FullName": "Romania",
- "Acronym": "ROU"
- },
- {
- "FlagName": "TK",
- "FullName": "Tokelau",
- "Acronym": "TKL"
- },
- {
- "FlagName": "GW",
- "FullName": "Guinea-Bissau",
- "Acronym": "GNB"
- },
- {
- "FlagName": "GU",
- "FullName": "Guam",
- "Acronym": "GUM"
- },
- {
- "FlagName": "GT",
- "FullName": "Guatemala",
- "Acronym": "GTM"
- },
- {
- "FlagName": "GS",
- "FullName": "South Georgia and the South Sandwich Islands",
- "Acronym": "SGS"
- },
- {
- "FlagName": "GR",
- "FullName": "Greece",
- "Acronym": "GRC"
- },
- {
- "FlagName": "GQ",
- "FullName": "Equatorial Guinea",
- "Acronym": "GNQ"
- },
- {
- "FlagName": "GP",
- "FullName": "Guadeloupe",
- "Acronym": "GLP"
- },
- {
- "FlagName": "JP",
- "FullName": "Japan",
- "Acronym": "JPN"
- },
- {
- "FlagName": "GY",
- "FullName": "Guyana",
- "Acronym": "GUY"
- },
- {
- "FlagName": "GG",
- "FullName": "Guernsey",
- "Acronym": "GGY"
- },
- {
- "FlagName": "GF",
- "FullName": "French Guiana",
- "Acronym": "GUF"
- },
- {
- "FlagName": "GE",
- "FullName": "Georgia",
- "Acronym": "GEO"
- },
- {
- "FlagName": "GD",
- "FullName": "Grenada",
- "Acronym": "GRD"
- },
- {
- "FlagName": "GB",
- "FullName": "United Kingdom",
- "Acronym": "GBR"
- },
- {
- "FlagName": "GA",
- "FullName": "Gabon",
- "Acronym": "GAB"
- },
- {
- "FlagName": "SV",
- "FullName": "El Salvador",
- "Acronym": "SLV"
- },
- {
- "FlagName": "GN",
- "FullName": "Guinea",
- "Acronym": "GIN"
- },
- {
- "FlagName": "GM",
- "FullName": "Gambia",
- "Acronym": "GMB"
- },
- {
- "FlagName": "GL",
- "FullName": "Greenland",
- "Acronym": "GRL"
- },
- {
- "FlagName": "GI",
- "FullName": "Gibraltar",
- "Acronym": "GIB"
- },
- {
- "FlagName": "GH",
- "FullName": "Ghana",
- "Acronym": "GHA"
- },
- {
- "FlagName": "OM",
- "FullName": "Oman",
- "Acronym": "OMN"
- },
- {
- "FlagName": "TN",
- "FullName": "Tunisia",
- "Acronym": "TUN"
- },
- {
- "FlagName": "JO",
- "FullName": "Jordan",
- "Acronym": "JOR"
- },
- {
- "FlagName": "HR",
- "FullName": "Croatia",
- "Acronym": "HRV"
- },
- {
- "FlagName": "HT",
- "FullName": "Haiti",
- "Acronym": "HTI"
- },
- {
- "FlagName": "HU",
- "FullName": "Hungary",
- "Acronym": "HUN"
- },
- {
- "FlagName": "HK",
- "FullName": "Hong Kong",
- "Acronym": "HKG"
- },
- {
- "FlagName": "HN",
- "FullName": "Honduras",
- "Acronym": "HND"
- },
- {
- "FlagName": "HM",
- "FullName": "Heard Island and McDonald Islands",
- "Acronym": "HMD"
- },
- {
- "FlagName": "VE",
- "FullName": "Venezuela",
- "Acronym": "VEN"
- },
- {
- "FlagName": "PR",
- "FullName": "Puerto Rico",
- "Acronym": "PRI"
- },
- {
- "FlagName": "PS",
- "FullName": "Palestinian Territory",
- "Acronym": "PSE"
- },
- {
- "FlagName": "PW",
- "FullName": "Palau",
- "Acronym": "PLW"
- },
- {
- "FlagName": "PT",
- "FullName": "Portugal",
- "Acronym": "PRT"
- },
- {
- "FlagName": "SJ",
- "FullName": "Svalbard and Jan Mayen",
- "Acronym": "SJM"
- },
- {
- "FlagName": "PY",
- "FullName": "Paraguay",
- "Acronym": "PRY"
- },
- {
- "FlagName": "IQ",
- "FullName": "Iraq",
- "Acronym": "IRQ"
- },
- {
- "FlagName": "PA",
- "FullName": "Panama",
- "Acronym": "PAN"
- },
- {
- "FlagName": "PF",
- "FullName": "French Polynesia",
- "Acronym": "PYF"
- },
- {
- "FlagName": "PG",
- "FullName": "Papua New Guinea",
- "Acronym": "PNG"
- },
- {
- "FlagName": "PE",
- "FullName": "Peru",
- "Acronym": "PER"
- },
- {
- "FlagName": "PK",
- "FullName": "Pakistan",
- "Acronym": "PAK"
- },
- {
- "FlagName": "PH",
- "FullName": "Philippines",
- "Acronym": "PHL"
- },
- {
- "FlagName": "PN",
- "FullName": "Pitcairn",
- "Acronym": "PCN"
- },
- {
- "FlagName": "PL",
- "FullName": "Poland",
- "Acronym": "POL"
- },
- {
- "FlagName": "PM",
- "FullName": "Saint Pierre and Miquelon",
- "Acronym": "SPM"
- },
- {
- "FlagName": "ZM",
- "FullName": "Zambia",
- "Acronym": "ZMB"
- },
- {
- "FlagName": "EH",
- "FullName": "Western Sahara",
- "Acronym": "ESH"
- },
- {
- "FlagName": "EE",
- "FullName": "Estonia",
- "Acronym": "EST"
- },
- {
- "FlagName": "EG",
- "FullName": "Egypt",
- "Acronym": "EGY"
- },
- {
- "FlagName": "ZA",
- "FullName": "South Africa",
- "Acronym": "ZAF"
- },
- {
- "FlagName": "EC",
- "FullName": "Ecuador",
- "Acronym": "ECU"
- },
- {
- "FlagName": "IT",
- "FullName": "Italy",
- "Acronym": "ITA"
- },
- {
- "FlagName": "VN",
- "FullName": "Vietnam",
- "Acronym": "VNM"
- },
- {
- "FlagName": "SB",
- "FullName": "Solomon Islands",
- "Acronym": "SLB"
- },
- {
- "FlagName": "ET",
- "FullName": "Ethiopia",
- "Acronym": "ETH"
- },
- {
- "FlagName": "SO",
- "FullName": "Somalia",
- "Acronym": "SOM"
- },
- {
- "FlagName": "ZW",
- "FullName": "Zimbabwe",
- "Acronym": "ZWE"
- },
- {
- "FlagName": "SA",
- "FullName": "Saudi Arabia",
- "Acronym": "SAU"
- },
- {
- "FlagName": "ES",
- "FullName": "Spain",
- "Acronym": "ESP"
- },
- {
- "FlagName": "ER",
- "FullName": "Eritrea",
- "Acronym": "ERI"
- },
- {
- "FlagName": "ME",
- "FullName": "Montenegro",
- "Acronym": "MNE"
- },
- {
- "FlagName": "MD",
- "FullName": "Moldova",
- "Acronym": "MDA"
- },
- {
- "FlagName": "MG",
- "FullName": "Madagascar",
- "Acronym": "MDG"
- },
- {
- "FlagName": "MF",
- "FullName": "Saint Martin",
- "Acronym": "MAF"
- },
- {
- "FlagName": "MA",
- "FullName": "Morocco",
- "Acronym": "MAR"
- },
- {
- "FlagName": "MC",
- "FullName": "Monaco",
- "Acronym": "MCO"
- },
- {
- "FlagName": "UZ",
- "FullName": "Uzbekistan",
- "Acronym": "UZB"
- },
- {
- "FlagName": "MM",
- "FullName": "Myanmar",
- "Acronym": "MMR"
- },
- {
- "FlagName": "ML",
- "FullName": "Mali",
- "Acronym": "MLI"
- },
- {
- "FlagName": "MO",
- "FullName": "Macao",
- "Acronym": "MAC"
- },
- {
- "FlagName": "MN",
- "FullName": "Mongolia",
- "Acronym": "MNG"
- },
- {
- "FlagName": "MH",
- "FullName": "Marshall Islands",
- "Acronym": "MHL"
- },
- {
- "FlagName": "MK",
- "FullName": "North Macedonia",
- "Acronym": "MKD"
- },
- {
- "FlagName": "MU",
- "FullName": "Mauritius",
- "Acronym": "MUS"
- },
- {
- "FlagName": "MT",
- "FullName": "Malta",
- "Acronym": "MLT"
- },
- {
- "FlagName": "MW",
- "FullName": "Malawi",
- "Acronym": "MWI"
- },
- {
- "FlagName": "MV",
- "FullName": "Maldives",
- "Acronym": "MDV"
- },
- {
- "FlagName": "MQ",
- "FullName": "Martinique",
- "Acronym": "MTQ"
- },
- {
- "FlagName": "MP",
- "FullName": "Northern Mariana Islands",
- "Acronym": "MNP"
- },
- {
- "FlagName": "MS",
- "FullName": "Montserrat",
- "Acronym": "MSR"
- },
- {
- "FlagName": "MR",
- "FullName": "Mauritania",
- "Acronym": "MRT"
- },
- {
- "FlagName": "IM",
- "FullName": "Isle of Man",
- "Acronym": "IMN"
- },
- {
- "FlagName": "UG",
- "FullName": "Uganda",
- "Acronym": "UGA"
- },
- {
- "FlagName": "TZ",
- "FullName": "Tanzania",
- "Acronym": "TZA"
- },
- {
- "FlagName": "MY",
- "FullName": "Malaysia",
- "Acronym": "MYS"
- },
- {
- "FlagName": "MX",
- "FullName": "Mexico",
- "Acronym": "MEX"
- },
- {
- "FlagName": "IL",
- "FullName": "Israel",
- "Acronym": "ISR"
- },
- {
- "FlagName": "FR",
- "FullName": "France",
- "Acronym": "FRA"
- },
- {
- "FlagName": "IO",
- "FullName": "British Indian Ocean Territory",
- "Acronym": "IOT"
- },
- {
- "FlagName": "SH",
- "FullName": "Saint Helena",
- "Acronym": "SHN"
- },
- {
- "FlagName": "FI",
- "FullName": "Finland",
- "Acronym": "FIN"
- },
- {
- "FlagName": "FJ",
- "FullName": "Fiji",
- "Acronym": "FJI"
- },
- {
- "FlagName": "FK",
- "FullName": "Falkland Islands",
- "Acronym": "FLK"
- },
- {
- "FlagName": "FM",
- "FullName": "Micronesia",
- "Acronym": "FSM"
- },
- {
- "FlagName": "FO",
- "FullName": "Faroe Islands",
- "Acronym": "FRO"
- },
- {
- "FlagName": "NI",
- "FullName": "Nicaragua",
- "Acronym": "NIC"
- },
- {
- "FlagName": "NL",
- "FullName": "Netherlands",
- "Acronym": "NLD"
- },
- {
- "FlagName": "NO",
- "FullName": "Norway",
- "Acronym": "NOR"
- },
- {
- "FlagName": "NA",
- "FullName": "Namibia",
- "Acronym": "NAM"
- },
- {
- "FlagName": "VU",
- "FullName": "Vanuatu",
- "Acronym": "VUT"
- },
- {
- "FlagName": "NC",
- "FullName": "New Caledonia",
- "Acronym": "NCL"
- },
- {
- "FlagName": "NE",
- "FullName": "Niger",
- "Acronym": "NER"
- },
- {
- "FlagName": "NF",
- "FullName": "Norfolk Island",
- "Acronym": "NFK"
- },
- {
- "FlagName": "NG",
- "FullName": "Nigeria",
- "Acronym": "NGA"
- },
- {
- "FlagName": "NZ",
- "FullName": "New Zealand",
- "Acronym": "NZL"
- },
- {
- "FlagName": "NP",
- "FullName": "Nepal",
- "Acronym": "NPL"
- },
- {
- "FlagName": "NR",
- "FullName": "Nauru",
- "Acronym": "NRU"
- },
- {
- "FlagName": "NU",
- "FullName": "Niue",
- "Acronym": "NIU"
- },
- {
- "FlagName": "CK",
- "FullName": "Cook Islands",
- "Acronym": "COK"
- },
- {
- "FlagName": "XK",
- "FullName": "Kosovo",
- "Acronym": "XKX"
- },
- {
- "FlagName": "CI",
- "FullName": "Ivory Coast",
- "Acronym": "CIV"
- },
- {
- "FlagName": "CH",
- "FullName": "Switzerland",
- "Acronym": "CHE"
- },
- {
- "FlagName": "CO",
- "FullName": "Colombia",
- "Acronym": "COL"
- },
- {
- "FlagName": "CN",
- "FullName": "China",
- "Acronym": "CHN"
- },
- {
- "FlagName": "CM",
- "FullName": "Cameroon",
- "Acronym": "CMR"
- },
- {
- "FlagName": "CL",
- "FullName": "Chile",
- "Acronym": "CHL"
- },
- {
- "FlagName": "CC",
- "FullName": "Cocos Islands",
- "Acronym": "CCK"
- },
- {
- "FlagName": "CA",
- "FullName": "Canada",
- "Acronym": "CAN"
- },
- {
- "FlagName": "CG",
- "FullName": "Republic of the Congo",
- "Acronym": "COG"
- },
- {
- "FlagName": "CF",
- "FullName": "Central African Republic",
- "Acronym": "CAF"
- },
- {
- "FlagName": "CD",
- "FullName": "Democratic Republic of the Congo",
- "Acronym": "COD"
- },
- {
- "FlagName": "CZ",
- "FullName": "Czech Republic",
- "Acronym": "CZE"
- },
- {
- "FlagName": "CY",
- "FullName": "Cyprus",
- "Acronym": "CYP"
- },
- {
- "FlagName": "CX",
- "FullName": "Christmas Island",
- "Acronym": "CXR"
- },
- {
- "FlagName": "CR",
- "FullName": "Costa Rica",
- "Acronym": "CRI"
- },
- {
- "FlagName": "CW",
- "FullName": "Curacao",
- "Acronym": "CUW"
- },
- {
- "FlagName": "CV",
- "FullName": "Cabo Verde",
- "Acronym": "CPV"
- },
- {
- "FlagName": "CU",
- "FullName": "Cuba",
- "Acronym": "CUB"
- },
- {
- "FlagName": "SZ",
- "FullName": "Eswatini",
- "Acronym": "SWZ"
- },
- {
- "FlagName": "SY",
- "FullName": "Syria",
- "Acronym": "SYR"
- },
- {
- "FlagName": "SX",
- "FullName": "Sint Maarten",
- "Acronym": "SXM"
- },
- {
- "FlagName": "KG",
- "FullName": "Kyrgyzstan",
- "Acronym": "KGZ"
- },
- {
- "FlagName": "KE",
- "FullName": "Kenya",
- "Acronym": "KEN"
- },
- {
- "FlagName": "SS",
- "FullName": "South Sudan",
- "Acronym": "SSD"
- },
- {
- "FlagName": "SR",
- "FullName": "Suriname",
- "Acronym": "SUR"
- },
- {
- "FlagName": "KI",
- "FullName": "Kiribati",
- "Acronym": "KIR"
- },
- {
- "FlagName": "KH",
- "FullName": "Cambodia",
- "Acronym": "KHM"
- },
- {
- "FlagName": "KN",
- "FullName": "Saint Kitts and Nevis",
- "Acronym": "KNA"
- },
- {
- "FlagName": "KM",
- "FullName": "Comoros",
- "Acronym": "COM"
- },
- {
- "FlagName": "ST",
- "FullName": "Sao Tome and Principe",
- "Acronym": "STP"
- },
- {
- "FlagName": "SK",
- "FullName": "Slovakia",
- "Acronym": "SVK"
- },
- {
- "FlagName": "KR",
- "FullName": "South Korea",
- "Acronym": "KOR"
- },
- {
- "FlagName": "SI",
- "FullName": "Slovenia",
- "Acronym": "SVN"
- },
- {
- "FlagName": "KP",
- "FullName": "North Korea",
- "Acronym": "PRK"
- },
- {
- "FlagName": "KW",
- "FullName": "Kuwait",
- "Acronym": "KWT"
- },
- {
- "FlagName": "SN",
- "FullName": "Senegal",
- "Acronym": "SEN"
- },
- {
- "FlagName": "SM",
- "FullName": "San Marino",
- "Acronym": "SMR"
- },
- {
- "FlagName": "SL",
- "FullName": "Sierra Leone",
- "Acronym": "SLE"
- },
- {
- "FlagName": "SC",
- "FullName": "Seychelles",
- "Acronym": "SYC"
- },
- {
- "FlagName": "KZ",
- "FullName": "Kazakhstan",
- "Acronym": "KAZ"
- },
- {
- "FlagName": "KY",
- "FullName": "Cayman Islands",
- "Acronym": "CYM"
- },
- {
- "FlagName": "SG",
- "FullName": "Singapore",
- "Acronym": "SGP"
- },
- {
- "FlagName": "SE",
- "FullName": "Sweden",
- "Acronym": "SWE"
- },
- {
- "FlagName": "SD",
- "FullName": "Sudan",
- "Acronym": "SDN"
- },
- {
- "FlagName": "DO",
- "FullName": "Dominican Republic",
- "Acronym": "DOM"
- },
- {
- "FlagName": "DM",
- "FullName": "Dominica",
- "Acronym": "DMA"
- },
- {
- "FlagName": "DJ",
- "FullName": "Djibouti",
- "Acronym": "DJI"
- },
- {
- "FlagName": "DK",
- "FullName": "Denmark",
- "Acronym": "DNK"
- },
- {
- "FlagName": "VG",
- "FullName": "British Virgin Islands",
- "Acronym": "VGB"
- },
- {
- "FlagName": "DE",
- "FullName": "Germany",
- "Acronym": "DEU"
- },
- {
- "FlagName": "YE",
- "FullName": "Yemen",
- "Acronym": "YEM"
- },
- {
- "FlagName": "DZ",
- "FullName": "Algeria",
- "Acronym": "DZA"
- },
- {
- "FlagName": "US",
- "FullName": "United States",
- "Acronym": "USA"
- },
- {
- "FlagName": "UY",
- "FullName": "Uruguay",
- "Acronym": "URY"
- },
- {
- "FlagName": "YT",
- "FullName": "Mayotte",
- "Acronym": "MYT"
- },
- {
- "FlagName": "UM",
- "FullName": "United States Minor Outlying Islands",
- "Acronym": "UMI"
- },
- {
- "FlagName": "LB",
- "FullName": "Lebanon",
- "Acronym": "LBN"
- },
- {
- "FlagName": "LC",
- "FullName": "Saint Lucia",
- "Acronym": "LCA"
- },
- {
- "FlagName": "LA",
- "FullName": "Laos",
- "Acronym": "LAO"
- },
- {
- "FlagName": "TV",
- "FullName": "Tuvalu",
- "Acronym": "TUV"
- },
- {
- "FlagName": "TW",
- "FullName": "Taiwan",
- "Acronym": "TWN"
- },
- {
- "FlagName": "TT",
- "FullName": "Trinidad and Tobago",
- "Acronym": "TTO"
- },
- {
- "FlagName": "TR",
- "FullName": "Turkey",
- "Acronym": "TUR"
- },
- {
- "FlagName": "LK",
- "FullName": "Sri Lanka",
- "Acronym": "LKA"
- },
- {
- "FlagName": "LI",
- "FullName": "Liechtenstein",
- "Acronym": "LIE"
- },
- {
- "FlagName": "LV",
- "FullName": "Latvia",
- "Acronym": "LVA"
- },
- {
- "FlagName": "TO",
- "FullName": "Tonga",
- "Acronym": "TON"
- },
- {
- "FlagName": "LT",
- "FullName": "Lithuania",
- "Acronym": "LTU"
- },
- {
- "FlagName": "LU",
- "FullName": "Luxembourg",
- "Acronym": "LUX"
- },
- {
- "FlagName": "LR",
- "FullName": "Liberia",
- "Acronym": "LBR"
- },
- {
- "FlagName": "LS",
- "FullName": "Lesotho",
- "Acronym": "LSO"
- },
- {
- "FlagName": "TH",
- "FullName": "Thailand",
- "Acronym": "THA"
- },
- {
- "FlagName": "TF",
- "FullName": "French Southern Territories",
- "Acronym": "ATF"
- },
- {
- "FlagName": "TG",
- "FullName": "Togo",
- "Acronym": "TGO"
- },
- {
- "FlagName": "TD",
- "FullName": "Chad",
- "Acronym": "TCD"
- },
- {
- "FlagName": "TC",
- "FullName": "Turks and Caicos Islands",
- "Acronym": "TCA"
- },
- {
- "FlagName": "LY",
- "FullName": "Libya",
- "Acronym": "LBY"
- },
- {
- "FlagName": "VA",
- "FullName": "Vatican",
- "Acronym": "VAT"
- },
- {
- "FlagName": "VC",
- "FullName": "Saint Vincent and the Grenadines",
- "Acronym": "VCT"
- },
- {
- "FlagName": "AE",
- "FullName": "United Arab Emirates",
- "Acronym": "ARE"
- },
- {
- "FlagName": "AD",
- "FullName": "Andorra",
- "Acronym": "AND"
- },
- {
- "FlagName": "AG",
- "FullName": "Antigua and Barbuda",
- "Acronym": "ATG"
- },
- {
- "FlagName": "AF",
- "FullName": "Afghanistan",
- "Acronym": "AFG"
- },
- {
- "FlagName": "AI",
- "FullName": "Anguilla",
- "Acronym": "AIA"
- },
- {
- "FlagName": "VI",
- "FullName": "U.S. Virgin Islands",
- "Acronym": "VIR"
- },
- {
- "FlagName": "IS",
- "FullName": "Iceland",
- "Acronym": "ISL"
- },
- {
- "FlagName": "IR",
- "FullName": "Iran",
- "Acronym": "IRN"
- },
- {
- "FlagName": "AM",
- "FullName": "Armenia",
- "Acronym": "ARM"
- },
- {
- "FlagName": "AL",
- "FullName": "Albania",
- "Acronym": "ALB"
- },
- {
- "FlagName": "AO",
- "FullName": "Angola",
- "Acronym": "AGO"
- },
- {
- "FlagName": "AQ",
- "FullName": "Antarctica",
- "Acronym": "ATA"
- },
- {
- "FlagName": "AS",
- "FullName": "American Samoa",
- "Acronym": "ASM"
- },
- {
- "FlagName": "AR",
- "FullName": "Argentina",
- "Acronym": "ARG"
- },
- {
- "FlagName": "AU",
- "FullName": "Australia",
- "Acronym": "AUS"
- },
- {
- "FlagName": "AT",
- "FullName": "Austria",
- "Acronym": "AUT"
- },
- {
- "FlagName": "AW",
- "FullName": "Aruba",
- "Acronym": "ABW"
- },
- {
- "FlagName": "IN",
- "FullName": "India",
- "Acronym": "IND"
- },
- {
- "FlagName": "AX",
- "FullName": "Aland Islands",
- "Acronym": "ALA"
- },
- {
- "FlagName": "AZ",
- "FullName": "Azerbaijan",
- "Acronym": "AZE"
- },
- {
- "FlagName": "IE",
- "FullName": "Ireland",
- "Acronym": "IRL"
- },
- {
- "FlagName": "ID",
- "FullName": "Indonesia",
- "Acronym": "IDN"
- },
- {
- "FlagName": "UA",
- "FullName": "Ukraine",
- "Acronym": "UKR"
- },
- {
- "FlagName": "QA",
- "FullName": "Qatar",
- "Acronym": "QAT"
- },
- {
- "FlagName": "MZ",
- "FullName": "Mozambique",
- "Acronym": "MOZ"
- }
-]
\ No newline at end of file
diff --git a/osu.Game.Tournament/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs
new file mode 100644
index 0000000000..b5e08fc005
--- /dev/null
+++ b/osu.Game.Tournament/SaveChangesOverlay.cs
@@ -0,0 +1,101 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Online.Multiplayer;
+using osuTK;
+
+namespace osu.Game.Tournament
+{
+ internal class SaveChangesOverlay : CompositeDrawable
+ {
+ [Resolved]
+ private TournamentGame tournamentGame { get; set; } = null!;
+
+ private string? lastSerialisedLadder;
+ private readonly TourneyButton saveChangesButton;
+
+ public SaveChangesOverlay()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = new Container
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Position = new Vector2(5),
+ CornerRadius = 10,
+ Masking = true,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ saveChangesButton = new TourneyButton
+ {
+ Text = "Save Changes",
+ Width = 140,
+ Height = 50,
+ Padding = new MarginPadding
+ {
+ Top = 10,
+ Left = 10,
+ },
+ Margin = new MarginPadding
+ {
+ Right = 10,
+ Bottom = 10,
+ },
+ Action = saveChanges,
+ // Enabled = { Value = false },
+ },
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ scheduleNextCheck();
+ }
+
+ private async Task checkForChanges()
+ {
+ string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder());
+
+ // If a save hasn't been triggered by the user yet, populate the initial value
+ lastSerialisedLadder ??= serialisedLadder;
+
+ if (lastSerialisedLadder != serialisedLadder && !saveChangesButton.Enabled.Value)
+ {
+ saveChangesButton.Enabled.Value = true;
+ saveChangesButton.Background
+ .FadeColour(saveChangesButton.BackgroundColour.Lighten(0.5f), 500, Easing.In).Then()
+ .FadeColour(saveChangesButton.BackgroundColour, 500, Easing.Out)
+ .Loop();
+ }
+
+ scheduleNextCheck();
+ }
+
+ private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
+
+ private void saveChanges()
+ {
+ tournamentGame.SaveChanges();
+ lastSerialisedLadder = tournamentGame.GetSerialisedLadder();
+
+ saveChangesButton.Enabled.Value = false;
+ saveChangesButton.Background.FadeColour(saveChangesButton.BackgroundColour, 500);
+ }
+ }
+}
diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
index 32da4d1b36..5ac25f97b5 100644
--- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
+++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs
@@ -12,8 +12,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics;
@@ -45,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Drawings
public ITeamList TeamList;
[BackgroundDependencyLoader]
- private void load(TextureStore textures, Storage storage)
+ private void load(Storage storage)
{
RelativeSizeAxes = Axes.Both;
@@ -91,11 +89,10 @@ namespace osu.Game.Tournament.Screens.Drawings
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- new Sprite
+ new TourneyVideo("drawings")
{
+ Loop = true,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fill,
- Texture = textures.Get(@"Backgrounds/Drawings/background.png")
},
// Visualiser
new VisualiserContainer
diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
index 11db37c8b7..da27c09e01 100644
--- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs
@@ -3,13 +3,13 @@
#nullable disable
+using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.IO;
using System.Linq;
-using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -25,9 +25,6 @@ namespace osu.Game.Tournament.Screens.Editors
{
public class TeamEditorScreen : TournamentEditorScreen
{
- [Resolved]
- private TournamentGameBase game { get; set; }
-
protected override BindableList Storage => LadderInfo.Teams;
[BackgroundDependencyLoader]
@@ -45,11 +42,17 @@ namespace osu.Game.Tournament.Screens.Editors
private void addAllCountries()
{
- List countries;
+ var countries = new List();
- using (Stream stream = game.Resources.GetStream("Resources/countries.json"))
- using (var sr = new StreamReader(stream))
- countries = JsonConvert.DeserializeObject>(sr.ReadToEnd());
+ foreach (var country in Enum.GetValues(typeof(CountryCode)).Cast().Skip(1))
+ {
+ countries.Add(new TournamentTeam
+ {
+ FlagName = { Value = country.ToString() },
+ FullName = { Value = country.GetDescription() },
+ Acronym = { Value = country.GetAcronym() },
+ });
+ }
Debug.Assert(countries != null);
@@ -298,10 +301,10 @@ namespace osu.Game.Tournament.Screens.Editors
}, true);
}
- private void updatePanel()
+ private void updatePanel() => Scheduler.AddOnce(() =>
{
drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 };
- }
+ });
}
}
}
diff --git a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
index 8af5bbe513..0fefe6f780 100644
--- a/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/TournamentEditorScreen.cs
@@ -20,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Editors
{
- public abstract class TournamentEditorScreen : TournamentScreen, IProvideVideo
+ public abstract class TournamentEditorScreen : TournamentScreen
where TDrawable : Drawable, IModelBacked
where TModel : class, new()
{
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
index bb187c9e67..1eceddd871 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs
@@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{
private readonly TeamScore score;
+ private readonly TournamentSpriteTextWithBackground teamText;
+
+ private readonly Bindable teamName = new Bindable("???");
+
private bool showScore;
public bool ShowScore
@@ -93,7 +97,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
}
}
},
- new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???")
+ teamText = new TournamentSpriteTextWithBackground
{
Scale = new Vector2(0.5f),
Origin = anchor,
@@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
updateDisplay();
FinishTransforms(true);
+
+ if (Team != null)
+ teamName.BindTo(Team.FullName);
+
+ teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
}
private void updateDisplay()
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
index ed11f097ed..5ee57e9271 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs
@@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
currentMatch.BindTo(ladder.CurrentMatch);
currentMatch.BindValueChanged(matchChanged);
+ currentTeam.BindValueChanged(teamChanged);
+
updateMatch();
}
@@ -67,7 +69,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
// team may change to same team, which means score is not in a good state.
// thus we handle this manually.
- teamChanged(currentTeam.Value);
+ currentTeam.TriggerChange();
}
protected override bool OnMouseDown(MouseDownEvent e)
@@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
return base.OnMouseDown(e);
}
- private void teamChanged(TournamentTeam team)
+ private void teamChanged(ValueChangedEvent team)
{
InternalChildren = new Drawable[]
{
- teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
+ teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
};
}
}
diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
index 86b2c2a4e9..54ae4c0366 100644
--- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs
@@ -21,7 +21,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Gameplay
{
- public class GameplayScreen : BeatmapInfoScreen, IProvideVideo
+ public class GameplayScreen : BeatmapInfoScreen
{
private readonly BindableBool warmup = new BindableBool();
diff --git a/osu.Game.Tournament/Screens/IProvideVideo.cs b/osu.Game.Tournament/Screens/IProvideVideo.cs
deleted file mode 100644
index aa67a5211f..0000000000
--- a/osu.Game.Tournament/Screens/IProvideVideo.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-namespace osu.Game.Tournament.Screens
-{
- ///
- /// Marker interface for a screen which provides its own local video background.
- ///
- public interface IProvideVideo
- {
- }
-}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
index f0eda5672a..1fdf616e34 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -53,6 +53,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection =>
{
+ // ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
+ GetContainingInputManager().TriggerFocusContention(null);
+
roundDropdown.Current = selection.NewValue?.Round;
losersCheckbox.Current = selection.NewValue?.Losers;
dateTimeBox.Current = selection.NewValue?.Date;
diff --git a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
index 23bfa84afc..7ad7e76a1f 100644
--- a/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
+++ b/osu.Game.Tournament/Screens/Ladder/LadderScreen.cs
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Ladder
{
- public class LadderScreen : TournamentScreen, IProvideVideo
+ public class LadderScreen : TournamentScreen
{
protected Container MatchesContainer;
private Container paths;
diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
index 7a11e26794..0827cbae69 100644
--- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
+++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Schedule
{
- public class ScheduleScreen : TournamentScreen // IProvidesVideo
+ public class ScheduleScreen : TournamentScreen
{
private readonly Bindable currentMatch = new Bindable();
private Container mainContainer;
diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
index 42eff3565f..2b2dce3664 100644
--- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
+++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs
@@ -9,6 +9,8 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Overlays;
@@ -19,7 +21,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Setup
{
- public class SetupScreen : TournamentScreen, IProvideVideo
+ public class SetupScreen : TournamentScreen
{
private FillFlowContainer fillFlow;
@@ -48,13 +50,21 @@ namespace osu.Game.Tournament.Screens.Setup
{
windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize);
- InternalChild = fillFlow = new FillFlowContainer
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Padding = new MarginPadding(10),
- Spacing = new Vector2(10),
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.2f),
+ },
+ fillFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(10),
+ Spacing = new Vector2(10),
+ }
};
api.LocalUser.BindValueChanged(_ => Schedule(reload));
@@ -74,7 +84,8 @@ namespace osu.Game.Tournament.Screens.Setup
Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()),
Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found",
Failing = fileBasedIpc?.IPCStorage == null,
- Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
+ Description =
+ "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
},
new ActionableInfo
{
diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
index 082aa99b0e..a7a175ceba 100644
--- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
+++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs
@@ -14,7 +14,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Showcase
{
- public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo
+ public class ShowcaseScreen : BeatmapInfoScreen
{
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
index 925c697346..9262cab098 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -19,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro
{
- public class SeedingScreen : TournamentMatchScreen, IProvideVideo
+ public class SeedingScreen : TournamentMatchScreen
{
private Container mainContainer;
@@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
currentTeam.BindValueChanged(teamChanged, true);
}
- private void teamChanged(ValueChangedEvent team)
+ private void teamChanged(ValueChangedEvent team) => Scheduler.AddOnce(() =>
{
if (team.NewValue == null)
{
@@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
}
showTeam(team.NewValue);
- }
+ });
protected override void CurrentMatchChanged(ValueChangedEvent match)
{
@@ -120,8 +121,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro
foreach (var seeding in team.SeedingResults)
{
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
+
foreach (var beatmap in seeding.Beatmaps)
+ {
+ if (beatmap.Beatmap == null)
+ continue;
+
fill.Add(new BeatmapScoreRow(beatmap));
+ }
}
}
@@ -129,6 +136,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
{
public BeatmapScoreRow(SeedingBeatmap beatmap)
{
+ Debug.Assert(beatmap.Beatmap != null);
+
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Children = new Drawable[]
{
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
- new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
+ new TournamentSpriteText
+ { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
}
},
};
diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
index 98dfaa7487..08c9a7a897 100644
--- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs
@@ -13,7 +13,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro
{
- public class TeamIntroScreen : TournamentMatchScreen, IProvideVideo
+ public class TeamIntroScreen : TournamentMatchScreen
{
private Container mainContainer;
diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
index 50207547cd..ac54ff58f5 100644
--- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
+++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs
@@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamWin
{
- public class TeamWinScreen : TournamentMatchScreen, IProvideVideo
+ public class TeamWinScreen : TournamentMatchScreen
{
private Container mainContainer;
@@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
private bool firstDisplay = true;
- private void update() => Schedule(() =>
+ private void update() => Scheduler.AddOnce(() =>
{
var match = CurrentMatch.Value;
diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index 537fbfc038..7d67bfa759 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -11,8 +11,6 @@ using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Logging;
using osu.Framework.Platform;
@@ -20,11 +18,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.Models;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tournament
{
+ [Cached]
public class TournamentGame : TournamentGameBase
{
public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE;
@@ -78,40 +76,9 @@ namespace osu.Game.Tournament
LoadComponentsAsync(new[]
{
- new Container
+ new SaveChangesOverlay
{
- CornerRadius = 10,
Depth = float.MinValue,
- Position = new Vector2(5),
- Masking = true,
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Children = new Drawable[]
- {
- new Box
- {
- Colour = OsuColour.Gray(0.2f),
- RelativeSizeAxes = Axes.Both,
- },
- new TourneyButton
- {
- Text = "Save Changes",
- Width = 140,
- Height = 50,
- Padding = new MarginPadding
- {
- Top = 10,
- Left = 10,
- },
- Margin = new MarginPadding
- {
- Right = 10,
- Bottom = 10,
- },
- Action = SaveChanges,
- },
- }
},
heightWarning = new WarningBox("Please make the window wider")
{
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index 75c9f17d4c..063b62bf08 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -21,6 +21,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.IO;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models;
+using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tournament
@@ -186,7 +187,9 @@ namespace osu.Game.Tournament
{
var playersRequiringPopulation = ladder.Teams
.SelectMany(t => t.Players)
- .Where(p => string.IsNullOrEmpty(p.Username) || p.Rank == null).ToList();
+ .Where(p => string.IsNullOrEmpty(p.Username)
+ || p.CountryCode == CountryCode.Unknown
+ || p.Rank == null).ToList();
if (playersRequiringPopulation.Count == 0)
return false;
@@ -288,14 +291,14 @@ namespace osu.Game.Tournament
user.Username = res.Username;
user.CoverUrl = res.CoverUrl;
- user.Country = res.Country;
+ user.CountryCode = res.CountryCode;
user.Rank = res.Statistics?.GlobalRank;
success?.Invoke();
}
}
- protected virtual void SaveChanges()
+ public void SaveChanges()
{
if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)
{
@@ -311,7 +314,16 @@ namespace osu.Game.Tournament
.ToList();
// Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state.
- string serialisedLadder = JsonConvert.SerializeObject(ladder,
+ string serialisedLadder = GetSerialisedLadder();
+
+ using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
+ using (var sw = new StreamWriter(stream))
+ sw.Write(serialisedLadder);
+ }
+
+ public string GetSerialisedLadder()
+ {
+ return JsonConvert.SerializeObject(ladder,
new JsonSerializerSettings
{
Formatting = Formatting.Indented,
@@ -319,10 +331,6 @@ namespace osu.Game.Tournament
DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() }
});
-
- using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
- using (var sw = new StreamWriter(stream))
- sw.Write(serialisedLadder);
}
protected override UserInputManager CreateUserInputManager() => new TournamentInputManager();
diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs
index 296b259d72..a12dbb4740 100644
--- a/osu.Game.Tournament/TournamentSceneManager.cs
+++ b/osu.Game.Tournament/TournamentSceneManager.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -186,7 +187,7 @@ namespace osu.Game.Tournament
var lastScreen = currentScreen;
currentScreen = target;
- if (currentScreen is IProvideVideo)
+ if (currentScreen.ChildrenOfType().FirstOrDefault()?.VideoAvailable == true)
{
video.FadeOut(200);
diff --git a/osu.Game.Tournament/TourneyButton.cs b/osu.Game.Tournament/TourneyButton.cs
index f5a82771f5..f1b14df783 100644
--- a/osu.Game.Tournament/TourneyButton.cs
+++ b/osu.Game.Tournament/TourneyButton.cs
@@ -3,12 +3,15 @@
#nullable disable
+using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tournament
{
public class TourneyButton : OsuButton
{
+ public new Box Background => base.Background;
+
public TourneyButton()
: base(null)
{
diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs
index 92f1fc17d5..3e4d01a9a3 100644
--- a/osu.Game/Beatmaps/BeatmapImporter.cs
+++ b/osu.Game/Beatmaps/BeatmapImporter.cs
@@ -80,9 +80,8 @@ namespace osu.Game.Beatmaps
if (beatmapSet.OnlineID > 0)
{
- var existingSetWithSameOnlineID = realm.All().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
-
- if (existingSetWithSameOnlineID != null)
+ // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure.
+ foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == beatmapSet.OnlineID))
{
existingSetWithSameOnlineID.DeletePending = true;
existingSetWithSameOnlineID.OnlineID = -1;
@@ -90,7 +89,7 @@ namespace osu.Game.Beatmaps
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineID = -1;
- LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted.");
+ LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion.");
}
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 346bf86818..41e89d864e 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
@@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
public bool SamplesMatchPlaybackRate { get; set; } = true;
+ ///
+ /// The time at which this beatmap was last played by the local user.
+ ///
+ public DateTimeOffset? LastPlayed { get; set; }
+
///
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see ).
@@ -151,14 +157,23 @@ namespace osu.Game.Beatmaps
public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
- && BeatmapSet.Hash == other.BeatmapSet.Hash
- && Metadata.AudioFile == other.Metadata.AudioFile;
+ && compareFiles(this, other, m => m.AudioFile);
public bool BackgroundEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
- && BeatmapSet.Hash == other.BeatmapSet.Hash
- && Metadata.BackgroundFile == other.Metadata.BackgroundFile;
+ && compareFiles(this, other, m => m.BackgroundFile);
+
+ private static bool compareFiles(BeatmapInfo x, BeatmapInfo y, Func getFilename)
+ {
+ Debug.Assert(x.BeatmapSet != null);
+ Debug.Assert(y.BeatmapSet != null);
+
+ string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash;
+ string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash;
+
+ return fileHashX == fileHashY;
+ }
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
diff --git a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs
index 58d13a3172..8002910b52 100644
--- a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs
+++ b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs
@@ -3,10 +3,10 @@
#nullable disable
-using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
+using osu.Game.Extensions;
namespace osu.Game.Beatmaps
{
@@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
- Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().Kebaberize()}");
+ Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().ToKebabCase()}");
}
}
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index ce883a7092..df44f01629 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -168,7 +168,7 @@ namespace osu.Game.Beatmaps
if (texture == null)
{
- Logger.Log($"Beatmap background failed to load (file {Metadata.BackgroundFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
+ Logger.Log($"Beatmap background failed to load (file {Metadata.BackgroundFile} not found on disk at expected location {fileStorePath}).");
return null;
}
diff --git a/osu.Game/Collections/CollectionToggleMenuItem.cs b/osu.Game/Collections/CollectionToggleMenuItem.cs
new file mode 100644
index 0000000000..f2b10305b8
--- /dev/null
+++ b/osu.Game/Collections/CollectionToggleMenuItem.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Collections
+{
+ public class CollectionToggleMenuItem : ToggleMenuItem
+ {
+ public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap)
+ : base(collection.Name.Value, MenuItemType.Standard, state =>
+ {
+ if (state)
+ collection.BeatmapHashes.Add(beatmap.MD5Hash);
+ else
+ collection.BeatmapHashes.Remove(beatmap.MD5Hash);
+ })
+ {
+ State.Value = collection.BeatmapHashes.Contains(beatmap.MD5Hash);
+ }
+ }
+}
diff --git a/osu.Game/Database/EFToRealmMigrator.cs b/osu.Game/Database/EFToRealmMigrator.cs
index 3b5424b3fb..294a8cd3ed 100644
--- a/osu.Game/Database/EFToRealmMigrator.cs
+++ b/osu.Game/Database/EFToRealmMigrator.cs
@@ -443,7 +443,6 @@ namespace osu.Game.Database
TotalScore = score.TotalScore,
MaxCombo = score.MaxCombo,
Accuracy = score.Accuracy,
- HasReplay = ((IScoreInfo)score).HasReplay,
Date = score.Date,
PP = score.PP,
Rank = score.Rank,
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 8cf57b802b..c4d65f4f10 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -58,8 +58,11 @@ namespace osu.Game.Database
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
+ /// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
+ /// 16 2022-07-15 Removed HasReplay from ScoreInfo.
+ /// 17 2022-07-16 Added CountryCode to RealmUser.
///
- private const int schema_version = 14;
+ private const int schema_version = 17;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs
index d1aba2bfe3..35f2d61437 100644
--- a/osu.Game/Extensions/DrawableExtensions.cs
+++ b/osu.Game/Extensions/DrawableExtensions.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -67,7 +66,7 @@ namespace osu.Game.Extensions
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
- if (!info.Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
+ if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
continue;
skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue);
diff --git a/osu.Game/Extensions/StringDehumanizeExtensions.cs b/osu.Game/Extensions/StringDehumanizeExtensions.cs
new file mode 100644
index 0000000000..6f0d7622d3
--- /dev/null
+++ b/osu.Game/Extensions/StringDehumanizeExtensions.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.
+
+// Based on code from the Humanizer library (https://github.com/Humanizr/Humanizer/blob/606e958cb83afc9be5b36716ac40d4daa9fa73a7/src/Humanizer/InflectorExtensions.cs)
+//
+// Humanizer is licenced under the MIT License (MIT)
+//
+// Copyright (c) .NET Foundation and Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+using System.Text.RegularExpressions;
+
+namespace osu.Game.Extensions
+{
+ ///
+ /// Class with extension methods used to turn human-readable strings to casing conventions frequently used in code.
+ /// Often used for communicating with other systems (web API, spectator server).
+ /// All of the operations in this class are intentionally culture-invariant.
+ ///
+ public static class StringDehumanizeExtensions
+ {
+ ///
+ /// Converts the string to "Pascal case" (also known as "upper camel case").
+ ///
+ ///
+ ///
+ /// "this is a test string".ToPascalCase() == "ThisIsATestString"
+ ///
+ ///
+ public static string ToPascalCase(this string input)
+ {
+ return Regex.Replace(input, "(?:^|_|-| +)(.)", match => match.Groups[1].Value.ToUpperInvariant());
+ }
+
+ ///
+ /// Converts the string to (lower) "camel case".
+ ///
+ ///
+ ///
+ /// "this is a test string".ToCamelCase() == "thisIsATestString"
+ ///
+ ///
+ public static string ToCamelCase(this string input)
+ {
+ string word = input.ToPascalCase();
+ return word.Length > 0 ? word.Substring(0, 1).ToLowerInvariant() + word.Substring(1) : word;
+ }
+
+ ///
+ /// Converts the string to "snake case".
+ ///
+ ///
+ ///
+ /// "this is a test string".ToSnakeCase() == "this_is_a_test_string"
+ ///
+ ///
+ public static string ToSnakeCase(this string input)
+ {
+ return Regex.Replace(
+ Regex.Replace(
+ Regex.Replace(input, @"([\p{Lu}]+)([\p{Lu}][\p{Ll}])", "$1_$2"), @"([\p{Ll}\d])([\p{Lu}])", "$1_$2"), @"[-\s]", "_").ToLowerInvariant();
+ }
+
+ ///
+ /// Converts the string to "kebab case".
+ ///
+ ///
+ ///
+ /// "this is a test string".ToKebabCase() == "this-is-a-test-string"
+ ///
+ ///
+ public static string ToKebabCase(this string input)
+ {
+ return ToSnakeCase(input).Replace('_', '-');
+ }
+ }
+}
diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs
index 89bdd09f0d..368ac56850 100644
--- a/osu.Game/IO/OsuStorage.cs
+++ b/osu.Game/IO/OsuStorage.cs
@@ -94,6 +94,8 @@ namespace osu.Game.IO
error = OsuStorageError.None;
Storage lastStorage = UnderlyingStorage;
+ Logger.Log($"Attempting to use custom storage location {CustomStoragePath}");
+
try
{
Storage userStorage = host.GetStorage(CustomStoragePath);
@@ -102,6 +104,7 @@ namespace osu.Game.IO
error = OsuStorageError.AccessibleButEmpty;
ChangeTargetStorage(userStorage);
+ Logger.Log($"Storage successfully changed to {CustomStoragePath}.");
}
catch
{
@@ -109,6 +112,9 @@ namespace osu.Game.IO
ChangeTargetStorage(lastStorage);
}
+ if (error != OsuStorageError.None)
+ Logger.Log($"Custom storage location could not be used ({error}).");
+
return error == OsuStorageError.None;
}
diff --git a/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs
index 4808ac1384..b51a8473ca 100644
--- a/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs
+++ b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs
@@ -3,8 +3,8 @@
#nullable disable
-using Humanizer;
using Newtonsoft.Json.Serialization;
+using osu.Game.Extensions;
namespace osu.Game.IO.Serialization
{
@@ -12,7 +12,7 @@ namespace osu.Game.IO.Serialization
{
protected override string ResolvePropertyName(string propertyName)
{
- return propertyName.Underscore();
+ return propertyName.ToSnakeCase();
}
}
}
diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs
index c13a1a10cb..6a4e5110e6 100644
--- a/osu.Game/Localisation/Language.cs
+++ b/osu.Game/Localisation/Language.cs
@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
+using JetBrains.Annotations;
namespace osu.Game.Localisation
{
+ [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public enum Language
{
[Description(@"English")]
diff --git a/osu.Game/Models/RealmUser.cs b/osu.Game/Models/RealmUser.cs
index 58fd7ff2a3..e20ffc0808 100644
--- a/osu.Game/Models/RealmUser.cs
+++ b/osu.Game/Models/RealmUser.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Game.Database;
using osu.Game.Users;
@@ -17,6 +15,16 @@ namespace osu.Game.Models
public string Username { get; set; } = string.Empty;
+ [Ignored]
+ public CountryCode CountryCode
+ {
+ get => Enum.TryParse(CountryString, out CountryCode country) ? country : CountryCode.Unknown;
+ set => CountryString = value.ToString();
+ }
+
+ [MapTo(nameof(CountryCode))]
+ public string CountryString { get; set; } = default(CountryCode).ToString();
+
public bool IsBot => false;
public bool Equals(RealmUser other)
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 088dc56701..7af19f6dd1 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Online.API
public string WebsiteRootUrl { get; }
- public int APIVersion => 20220217; // We may want to pull this from the game version eventually.
+ public int APIVersion => 20220705; // We may want to pull this from the game version eventually.
public Exception LastLoginError { get; private set; }
@@ -163,7 +163,13 @@ namespace osu.Game.Online.API
userReq.Failure += ex =>
{
- if (ex is WebException webException && webException.Message == @"Unauthorized")
+ if (ex is APIException)
+ {
+ LastLoginError = ex;
+ log.Add("Login failed on local user retrieval!");
+ Logout();
+ }
+ else if (ex is WebException webException && webException.Message == @"Unauthorized")
{
log.Add(@"Login no longer valid");
Logout();
diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs
index dc1db08174..900f59290c 100644
--- a/osu.Game/Online/API/APIMod.cs
+++ b/osu.Game/Online/API/APIMod.cs
@@ -5,13 +5,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using Humanizer;
using MessagePack;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Configuration;
+using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@@ -44,11 +45,11 @@ namespace osu.Game.Online.API
var bindable = (IBindable)property.GetValue(mod);
if (!bindable.IsDefault)
- Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue());
+ Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue());
}
}
- public Mod ToMod(Ruleset ruleset)
+ public Mod ToMod([NotNull] Ruleset ruleset)
{
Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
@@ -62,10 +63,17 @@ namespace osu.Game.Online.API
{
foreach (var (_, property) in resultMod.GetSettingsSourceProperties())
{
- if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
+ if (!Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
continue;
- resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
+ try
+ {
+ resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
+ }
+ catch (Exception ex)
+ {
+ Logger.Log($"Failed to copy mod setting value '{settingValue ?? "null"}' to \"{property.Name}\": {ex.Message}");
+ }
}
}
diff --git a/osu.Game/Online/API/Requests/GetCommentsRequest.cs b/osu.Game/Online/API/Requests/GetCommentsRequest.cs
index c63c574124..1aa08f2ed8 100644
--- a/osu.Game/Online/API/Requests/GetCommentsRequest.cs
+++ b/osu.Game/Online/API/Requests/GetCommentsRequest.cs
@@ -4,7 +4,7 @@
#nullable disable
using osu.Framework.IO.Network;
-using Humanizer;
+using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Comments;
@@ -32,7 +32,7 @@ namespace osu.Game.Online.API.Requests
var req = base.CreateWebRequest();
req.AddParameter("commentable_id", commentableId.ToString());
- req.AddParameter("commentable_type", type.ToString().Underscore().ToLowerInvariant());
+ req.AddParameter("commentable_type", type.ToString().ToSnakeCase().ToLowerInvariant());
req.AddParameter("page", page.ToString());
req.AddParameter("sort", sort.ToString().ToLowerInvariant());
diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs
index a6cd9a52c7..966e69938c 100644
--- a/osu.Game/Online/API/Requests/GetScoresRequest.cs
+++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Online.API.Requests
this.mods = mods ?? Array.Empty();
}
- protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/scores{createQueryParameters()}";
+ protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/solo-scores{createQueryParameters()}";
private string createQueryParameters()
{
diff --git a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
index 3ec60cd06c..d723786f23 100644
--- a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs
@@ -3,8 +3,8 @@
#nullable disable
-using Humanizer;
using System.Collections.Generic;
+using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
@@ -22,7 +22,7 @@ namespace osu.Game.Online.API.Requests
this.type = type;
}
- protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().Underscore()}";
+ protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().ToSnakeCase()}";
}
public enum BeatmapSetType
diff --git a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs
index ab0cc3a56d..c27a83b695 100644
--- a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs
@@ -5,6 +5,7 @@
using osu.Framework.IO.Network;
using osu.Game.Rulesets;
+using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
@@ -12,21 +13,21 @@ namespace osu.Game.Online.API.Requests
{
public readonly UserRankingsType Type;
- private readonly string country;
+ private readonly CountryCode countryCode;
- public GetUserRankingsRequest(RulesetInfo ruleset, UserRankingsType type = UserRankingsType.Performance, int page = 1, string country = null)
+ public GetUserRankingsRequest(RulesetInfo ruleset, UserRankingsType type = UserRankingsType.Performance, int page = 1, CountryCode countryCode = CountryCode.Unknown)
: base(ruleset, page)
{
Type = type;
- this.country = country;
+ this.countryCode = countryCode;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
- if (country != null)
- req.AddParameter("country", country);
+ if (countryCode != CountryCode.Unknown)
+ req.AddParameter("country", countryCode.ToString());
return req;
}
diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
index 9bd78b7be1..8ef797f799 100644
--- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs
@@ -10,7 +10,7 @@ using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
- public class GetUserScoresRequest : PaginatedAPIRequest>
+ public class GetUserScoresRequest : PaginatedAPIRequest>
{
private readonly long userId;
private readonly ScoreType type;
diff --git a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
index 8fefe4d9c2..2def18926f 100644
--- a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs
@@ -4,8 +4,8 @@
#nullable disable
using System;
-using Humanizer;
using Newtonsoft.Json;
+using osu.Game.Extensions;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
@@ -21,7 +21,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string type
{
- set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize());
+ set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.ToPascalCase());
}
public RecentActivityType Type;
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
index 8bd54f889d..494826f534 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoreWithPosition.cs
@@ -16,11 +16,11 @@ namespace osu.Game.Online.API.Requests.Responses
public int? Position;
[JsonProperty(@"score")]
- public APIScore Score;
+ public SoloScoreInfo Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
- var score = Score.CreateScoreInfo(rulesets, beatmap);
+ var score = Score.ToScoreInfo(rulesets, beatmap);
score.Position = Position;
return score;
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
index 9c8a38c63a..4ef39be5e5 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIScoresCollection.cs
@@ -11,9 +11,9 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIScoresCollection
{
[JsonProperty(@"scores")]
- public List Scores;
+ public List Scores;
- [JsonProperty(@"userScore")]
+ [JsonProperty(@"user_score")]
public APIScoreWithPosition UserScore;
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs
index 63aaa9b90e..5f843e9a7b 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUser.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs
@@ -34,8 +34,19 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"previous_usernames")]
public string[] PreviousUsernames;
+ private CountryCode? countryCode;
+
+ public CountryCode CountryCode
+ {
+ get => countryCode ??= (Enum.TryParse(country?.Code, out CountryCode result) ? result : default);
+ set => countryCode = value;
+ }
+
+#pragma warning disable 649
+ [CanBeNull]
[JsonProperty(@"country")]
- public Country Country;
+ private Country country;
+#pragma warning restore 649
public readonly Bindable Status = new Bindable();
@@ -256,5 +267,13 @@ namespace osu.Game.Online.API.Requests.Responses
public int OnlineID => Id;
public bool Equals(APIUser other) => this.MatchesOnlineID(other);
+
+#pragma warning disable 649
+ private class Country
+ {
+ [JsonProperty(@"code")]
+ public string Code;
+ }
+#pragma warning restore 649
}
}
diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
new file mode 100644
index 0000000000..bfc8b4102a
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
@@ -0,0 +1,156 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ [Serializable]
+ public class SoloScoreInfo : IHasOnlineID
+ {
+ [JsonProperty("replay")]
+ public bool HasReplay { get; set; }
+
+ [JsonProperty("beatmap_id")]
+ public int BeatmapID { get; set; }
+
+ [JsonProperty("ruleset_id")]
+ public int RulesetID { get; set; }
+
+ [JsonProperty("build_id")]
+ public int? BuildID { get; set; }
+
+ [JsonProperty("passed")]
+ public bool Passed { get; set; }
+
+ [JsonProperty("total_score")]
+ public int TotalScore { get; set; }
+
+ [JsonProperty("accuracy")]
+ public double Accuracy { get; set; }
+
+ [JsonProperty("user_id")]
+ public int UserID { get; set; }
+
+ // TODO: probably want to update this column to match user stats (short)?
+ [JsonProperty("max_combo")]
+ public int MaxCombo { get; set; }
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ [JsonProperty("rank")]
+ public ScoreRank Rank { get; set; }
+
+ [JsonProperty("started_at")]
+ public DateTimeOffset? StartedAt { get; set; }
+
+ [JsonProperty("ended_at")]
+ public DateTimeOffset EndedAt { get; set; }
+
+ [JsonProperty("mods")]
+ public APIMod[] Mods { get; set; } = Array.Empty();
+
+ [JsonIgnore]
+ [JsonProperty("created_at")]
+ public DateTimeOffset CreatedAt { get; set; }
+
+ [JsonIgnore]
+ [JsonProperty("updated_at")]
+ public DateTimeOffset UpdatedAt { get; set; }
+
+ [JsonIgnore]
+ [JsonProperty("deleted_at")]
+ public DateTimeOffset? DeletedAt { get; set; }
+
+ [JsonProperty("statistics")]
+ public Dictionary Statistics { get; set; } = new Dictionary();
+
+ #region osu-web API additions (not stored to database).
+
+ [JsonProperty("id")]
+ public long? ID { get; set; }
+
+ [JsonProperty("user")]
+ public APIUser? User { get; set; }
+
+ [JsonProperty("beatmap")]
+ public APIBeatmap? Beatmap { get; set; }
+
+ [JsonProperty("beatmapset")]
+ public APIBeatmapSet? BeatmapSet
+ {
+ set
+ {
+ // in the deserialisation case we need to ferry this data across.
+ // the order of properties returned by the API guarantees that the beatmap is populated by this point.
+ if (!(Beatmap is APIBeatmap apiBeatmap))
+ throw new InvalidOperationException("Beatmap set metadata arrived before beatmap metadata in response");
+
+ apiBeatmap.BeatmapSet = value;
+ }
+ }
+
+ [JsonProperty("pp")]
+ public double? PP { get; set; }
+
+ #endregion
+
+ public override string ToString() => $"score_id: {ID} user_id: {UserID}";
+
+ ///
+ /// Create a from an API score instance.
+ ///
+ /// A ruleset store, used to populate a ruleset instance in the returned score.
+ /// An optional beatmap, copied into the returned score (for cases where the API does not populate the beatmap).
+ ///
+ public ScoreInfo ToScoreInfo(RulesetStore rulesets, BeatmapInfo? beatmap = null)
+ {
+ var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally");
+
+ var rulesetInstance = ruleset.CreateInstance();
+
+ var mods = Mods.Select(apiMod => apiMod.ToMod(rulesetInstance)).ToArray();
+
+ var scoreInfo = ToScoreInfo(mods);
+
+ scoreInfo.Ruleset = ruleset;
+ if (beatmap != null) scoreInfo.BeatmapInfo = beatmap;
+
+ return scoreInfo;
+ }
+
+ ///
+ /// Create a from an API score instance.
+ ///
+ /// The mod instances, resolved from a ruleset.
+ ///
+ public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo
+ {
+ OnlineID = OnlineID,
+ User = User ?? new APIUser { Id = UserID },
+ BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
+ Ruleset = new RulesetInfo { OnlineID = RulesetID },
+ Passed = Passed,
+ TotalScore = TotalScore,
+ Accuracy = Accuracy,
+ MaxCombo = MaxCombo,
+ Rank = Rank,
+ Statistics = Statistics,
+ Date = EndedAt,
+ Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
+ Mods = mods,
+ PP = PP,
+ };
+
+ public long OnlineID => ID ?? -1;
+ }
+}
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 082f9bb371..c303c410ec 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -5,7 +5,6 @@
using System.Collections.Generic;
using System.Linq;
-using Humanizer;
using JetBrains.Annotations;
using osu.Framework.IO.Network;
using osu.Game.Extensions;
@@ -86,7 +85,7 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("q", query);
if (General != null && General.Any())
- req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().Underscore())));
+ req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().ToSnakeCase())));
if (ruleset.OnlineID >= 0)
req.AddParameter("m", ruleset.OnlineID.ToString());
diff --git a/osu.Game/Online/DownloadState.cs b/osu.Game/Online/DownloadState.cs
index 3d389d45f9..a58c40d16a 100644
--- a/osu.Game/Online/DownloadState.cs
+++ b/osu.Game/Online/DownloadState.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
-
namespace osu.Game.Online
{
public enum DownloadState
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index 01f0f3a902..6bfe09e911 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -64,26 +64,26 @@ namespace osu.Game.Online
this.preferMessagePack = preferMessagePack;
apiState.BindTo(api.State);
- apiState.BindValueChanged(_ => connectIfPossible(), true);
+ apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
}
- public void Reconnect()
+ public Task Reconnect()
{
Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network);
- Task.Run(connectIfPossible);
+ return Task.Run(connectIfPossible);
}
- private void connectIfPossible()
+ private async Task connectIfPossible()
{
switch (apiState.Value)
{
case APIState.Failing:
case APIState.Offline:
- Task.Run(() => disconnect(true));
+ await disconnect(true);
break;
case APIState.Online:
- Task.Run(connect);
+ await connect();
break;
}
}
diff --git a/osu.Game/Online/IHubClientConnector.cs b/osu.Game/Online/IHubClientConnector.cs
index 2afab9091b..53c4897e73 100644
--- a/osu.Game/Online/IHubClientConnector.cs
+++ b/osu.Game/Online/IHubClientConnector.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Bindables;
using osu.Game.Online.API;
@@ -32,6 +33,6 @@ namespace osu.Game.Online
///
/// Reconnect if already connected.
///
- void Reconnect();
+ Task Reconnect();
}
}
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 62827f50aa..a7b6bd044d 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -181,7 +181,7 @@ namespace osu.Game.Online.Leaderboards
Masking = true,
Children = new Drawable[]
{
- new UpdateableFlag(user.Country)
+ new UpdateableFlag(user.CountryCode)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 9832acb140..603bd10c38 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -21,7 +21,6 @@ using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
-using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Online.Multiplayer
{
@@ -91,7 +90,7 @@ namespace osu.Game.Online.Multiplayer
///
/// The joined .
///
- public virtual MultiplayerRoom? Room
+ public virtual MultiplayerRoom? Room // virtual for moq
{
get
{
@@ -150,7 +149,7 @@ namespace osu.Game.Online.Multiplayer
// clean up local room state on server disconnect.
if (!connected.NewValue && Room != null)
{
- Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important);
+ Logger.Log("Clearing room due to multiplayer server connection loss.", LoggingTarget.Runtime, LogLevel.Important);
LeaveRoom();
}
}));
diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
index c061398209..190d150502 100644
--- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs
@@ -85,7 +85,13 @@ namespace osu.Game.Online.Multiplayer
catch (HubException exception)
{
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
- connector?.Reconnect();
+ {
+ Debug.Assert(connector != null);
+
+ await connector.Reconnect();
+ return await JoinRoom(roomId, password);
+ }
+
throw;
}
}
diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs
index 5ae9d58189..afab83b5be 100644
--- a/osu.Game/Online/Rooms/GetRoomsRequest.cs
+++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs
@@ -4,8 +4,8 @@
#nullable disable
using System.Collections.Generic;
-using Humanizer;
using osu.Framework.IO.Network;
+using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@@ -27,7 +27,7 @@ namespace osu.Game.Online.Rooms
var req = base.CreateWebRequest();
if (status != RoomStatusFilter.Open)
- req.AddParameter("mode", status.ToString().Underscore().ToLowerInvariant());
+ req.AddParameter("mode", status.ToString().ToSnakeCase().ToLowerInvariant());
if (!string.IsNullOrEmpty(category))
req.AddParameter("category", category);
diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
index cd9f5233a2..030ca724c4 100644
--- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
+++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
@@ -56,13 +56,20 @@ namespace osu.Game.Online.Spectator
try
{
- await connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state);
+ await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
}
catch (HubException exception)
{
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
- connector?.Reconnect();
- throw;
+ {
+ Debug.Assert(connector != null);
+
+ await connector.Reconnect();
+ await BeginPlayingInternal(state);
+ }
+
+ // Exceptions can occur if, for instance, the locally played beatmap doesn't have a server-side counterpart.
+ // For now, let's ignore these so they don't cause unobserved exceptions to appear to the user (and sentry).
}
}
@@ -73,7 +80,7 @@ namespace osu.Game.Online.Spectator
Debug.Assert(connection != null);
- return connection.SendAsync(nameof(ISpectatorServer.SendFrameData), bundle);
+ return connection.InvokeAsync(nameof(ISpectatorServer.SendFrameData), bundle);
}
protected override Task EndPlayingInternal(SpectatorState state)
@@ -83,7 +90,7 @@ namespace osu.Game.Online.Spectator
Debug.Assert(connection != null);
- return connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), state);
+ return connection.InvokeAsync(nameof(ISpectatorServer.EndPlaySession), state);
}
protected override Task WatchUserInternal(int userId)
@@ -93,7 +100,7 @@ namespace osu.Game.Online.Spectator
Debug.Assert(connection != null);
- return connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
+ return connection.InvokeAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
}
protected override Task StopWatchingUserInternal(int userId)
@@ -103,7 +110,7 @@ namespace osu.Game.Online.Spectator
Debug.Assert(connection != null);
- return connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId);
+ return connection.InvokeAsync(nameof(ISpectatorServer.EndWatchingUser), userId);
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index 767b8646e3..2ca369d459 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -143,7 +143,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
public void Search(string query)
- => searchControl.Query.Value = query;
+ => Schedule(() => searchControl.Query.Value = query);
protected override void LoadComplete()
{
diff --git a/osu.Game/Overlays/BeatmapSet/MetadataSection.cs b/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
index c6bf94f507..317b369d8f 100644
--- a/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/MetadataSection.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -22,11 +23,14 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly MetadataType type;
private TextFlowContainer textFlow;
+ private readonly Action searchAction;
+
private const float transition_duration = 250;
- public MetadataSection(MetadataType type)
+ public MetadataSection(MetadataType type, Action searchAction = null)
{
this.type = type;
+ this.searchAction = searchAction;
Alpha = 0;
@@ -91,7 +95,12 @@ namespace osu.Game.Overlays.BeatmapSet
for (int i = 0; i <= tags.Length - 1; i++)
{
- loaded.AddLink(tags[i], LinkAction.SearchBeatmapSet, tags[i]);
+ string tag = tags[i];
+
+ if (searchAction != null)
+ loaded.AddLink(tag, () => searchAction(tag));
+ else
+ loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
if (i != tags.Length - 1)
loaded.AddText(" ");
@@ -100,7 +109,11 @@ namespace osu.Game.Overlays.BeatmapSet
break;
case MetadataType.Source:
- loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
+ if (searchAction != null)
+ loaded.AddLink(text, () => searchAction(text));
+ else
+ loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
+
break;
default:
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index b2e5be6601..6acc9bf002 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -165,10 +165,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Font = OsuFont.GetFont(size: text_size),
Colour = score.Accuracy == 1 ? highAccuracyColour : Color4.White
},
- new UpdateableFlag(score.User.Country)
+ new UpdateableFlag(score.User.CountryCode)
{
Size = new Vector2(19, 14),
- ShowPlaceholderOnNull = false,
+ ShowPlaceholderOnUnknown = false,
},
username,
new OsuSpriteText
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index c2e54d0d7b..e50fc356eb 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
MD5Hash = apiBeatmap.MD5Hash
};
- scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
+ scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
.ContinueWith(task => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
@@ -101,7 +101,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
scoreTable.Show();
var userScore = value.UserScore;
- var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets, beatmapInfo);
+ var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo);
topScoresContainer.Add(new DrawableTopScore(topScore));
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
index f093c6b53f..2eaa03a05d 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Origin = Anchor.CentreLeft,
Size = new Vector2(19, 14),
Margin = new MarginPadding { Top = 3 }, // makes spacing look more even
- ShowPlaceholderOnNull = false,
+ ShowPlaceholderOnUnknown = false,
},
}
}
@@ -141,7 +141,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
set
{
avatar.User = value.User;
- flag.Country = value.User.Country;
+ flag.CountryCode = value.User.CountryCode;
achievedOn.Date = value.Date;
usernameText.Clear();
diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index 5961298485..4c425d3d4c 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.cs
@@ -257,10 +257,14 @@ namespace osu.Game.Overlays.Chat
}
[BackgroundDependencyLoader]
- private void load(UserProfileOverlay? profile, ChannelManager? chatManager)
+ private void load(UserProfileOverlay? profile, ChannelManager? chatManager, ChatOverlay? chatOverlay)
{
Action = () => profile?.ShowUser(sender);
- startChatAction = () => chatManager?.OpenPrivateChannel(sender);
+ startChatAction = () =>
+ {
+ chatManager?.OpenPrivateChannel(sender);
+ chatOverlay?.Show();
+ };
}
public MenuItem[] ContextMenuItems
diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs
index a07cf1608d..493cd66258 100644
--- a/osu.Game/Overlays/DialogOverlay.cs
+++ b/osu.Game/Overlays/DialogOverlay.cs
@@ -49,43 +49,54 @@ namespace osu.Game.Overlays
public void Push(PopupDialog dialog)
{
- if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
-
- var lastDialog = CurrentDialog;
+ if (dialog == CurrentDialog || dialog.State.Value == Visibility.Hidden) return;
// Immediately update the externally accessible property as this may be used for checks even before
// a DialogOverlay instance has finished loading.
+ var lastDialog = CurrentDialog;
CurrentDialog = dialog;
- Scheduler.Add(() =>
+ Schedule(() =>
{
// if any existing dialog is being displayed, dismiss it before showing a new one.
lastDialog?.Hide();
- dialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
- dialogContainer.Add(dialog);
+ // if the new dialog is hidden before added to the dialogContainer, bypass any further operations.
+ if (dialog.State.Value == Visibility.Hidden)
+ {
+ dismiss();
+ return;
+ }
+
+ dialogContainer.Add(dialog);
Show();
- }, false);
+
+ dialog.State.BindValueChanged(state =>
+ {
+ if (state.NewValue != Visibility.Hidden) return;
+
+ // Trigger the demise of the dialog as soon as it hides.
+ dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
+
+ dismiss();
+ });
+ });
+
+ void dismiss()
+ {
+ if (dialog != CurrentDialog) return;
+
+ // Handle the case where the dialog is the currently displayed dialog.
+ // In this scenario, the overlay itself should also be hidden.
+ Hide();
+ CurrentDialog = null;
+ }
}
public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0;
protected override bool BlockNonPositionalInput => true;
- private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v)
- {
- if (v != Visibility.Hidden) return;
-
- // handle the dialog being dismissed.
- dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
-
- if (dialog == CurrentDialog)
- {
- Hide();
- CurrentDialog = null;
- }
- }
-
protected override void PopIn()
{
base.PopIn();
@@ -97,7 +108,8 @@ namespace osu.Game.Overlays
base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
- if (CurrentDialog?.State.Value == Visibility.Visible)
+ // PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present.
+ if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible)
CurrentDialog.Hide();
}
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
index 20ff8f21c8..cb1e96d2f2 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
@@ -1,14 +1,24 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System;
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Configuration;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Framework.Threading;
+using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
+using osuTK;
namespace osu.Game.Overlays.FirstRunSetup
{
@@ -20,13 +30,175 @@ namespace osu.Game.Overlays.FirstRunSetup
{
Content.Children = new Drawable[]
{
- new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ RowDimensions = new[]
+ {
+ // Avoid height changes when changing language.
+ new Dimension(GridSizeMode.AutoSize, minSize: 100),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
+ {
+ Text = FirstRunSetupOverlayStrings.WelcomeDescription,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ },
+ },
+ }
+ },
+ new LanguageSelectionFlow
{
- Text = FirstRunSetupOverlayStrings.WelcomeDescription,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
- },
+ }
};
}
+
+ private class LanguageSelectionFlow : FillFlowContainer
+ {
+ private Bindable frameworkLocale = null!;
+
+ private ScheduledDelegate? updateSelectedDelegate;
+
+ [BackgroundDependencyLoader]
+ private void load(FrameworkConfigManager frameworkConfig)
+ {
+ Direction = FillDirection.Full;
+ Spacing = new Vector2(5);
+
+ ChildrenEnumerable = Enum.GetValues(typeof(Language))
+ .Cast()
+ .Select(l => new LanguageButton(l)
+ {
+ Action = () => frameworkLocale.Value = l.ToCultureCode()
+ });
+
+ frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale);
+ frameworkLocale.BindValueChanged(locale =>
+ {
+ if (!LanguageExtensions.TryParseCultureCode(locale.NewValue, out var language))
+ language = Language.en;
+
+ // Changing language may cause a short period of blocking the UI thread while the new glyphs are loaded.
+ // Scheduling ensures the button animation plays smoothly after any blocking operation completes.
+ // Note that a delay is required (the alternative would be a double-schedule; delay feels better).
+ updateSelectedDelegate?.Cancel();
+ updateSelectedDelegate = Scheduler.AddDelayed(() => updateSelectedStates(language), 50);
+ }, true);
+ }
+
+ private void updateSelectedStates(Language language)
+ {
+ foreach (var c in Children.OfType())
+ c.Selected = c.Language == language;
+ }
+
+ private class LanguageButton : OsuClickableContainer
+ {
+ public readonly Language Language;
+
+ private Box backgroundBox = null!;
+
+ private OsuSpriteText text = null!;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; } = null!;
+
+ private bool selected;
+
+ public bool Selected
+ {
+ get => selected;
+ set
+ {
+ if (selected == value)
+ return;
+
+ selected = value;
+
+ if (IsLoaded)
+ updateState();
+ }
+ }
+
+ public LanguageButton(Language language)
+ {
+ Language = language;
+
+ Size = new Vector2(160, 50);
+ Masking = true;
+ CornerRadius = 10;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ backgroundBox = new Box
+ {
+ Alpha = 0,
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
+ },
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = colourProvider.Light1,
+ Text = Language.GetDescription(),
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateState();
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ if (!selected)
+ updateState();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ if (!selected)
+ updateState();
+ base.OnHoverLost(e);
+ }
+
+ private void updateState()
+ {
+ if (selected)
+ {
+ const double selected_duration = 1000;
+
+ backgroundBox.FadeTo(1, selected_duration, Easing.OutQuint);
+ backgroundBox.FadeColour(colourProvider.Background2, selected_duration, Easing.OutQuint);
+ text.FadeColour(colourProvider.Content1, selected_duration, Easing.OutQuint);
+ text.ScaleTo(1.2f, selected_duration, Easing.OutQuint);
+ }
+ else
+ {
+ const double duration = 500;
+
+ backgroundBox.FadeTo(IsHovered ? 1 : 0, duration, Easing.OutQuint);
+ backgroundBox.FadeColour(colourProvider.Background5, duration, Easing.OutQuint);
+ text.FadeColour(colourProvider.Light1, duration, Easing.OutQuint);
+ text.ScaleTo(1, duration, Easing.OutQuint);
+ }
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
index 9c957e387a..7e079c8341 100644
--- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
@@ -5,6 +5,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
@@ -136,7 +137,7 @@ namespace osu.Game.Overlays.Profile.Header
userFlag = new UpdateableFlag
{
Size = new Vector2(28, 20),
- ShowPlaceholderOnNull = false,
+ ShowPlaceholderOnUnknown = false,
},
userCountryText = new OsuSpriteText
{
@@ -174,8 +175,8 @@ namespace osu.Game.Overlays.Profile.Header
avatar.User = user;
usernameText.Text = user?.Username ?? string.Empty;
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
- userFlag.Country = user?.Country;
- userCountryText.Text = user?.Country?.FullName ?? "Alien";
+ userFlag.CountryCode = user?.CountryCode ?? default;
+ userCountryText.Text = (user?.CountryCode ?? default).GetDescription();
supporterTag.SupportLevel = user?.SupportLevel ?? 0;
titleText.Text = user?.Title ?? string.Empty;
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
index 90a357a281..5d8f8c8326 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
private const float performance_background_shear = 0.45f;
- protected readonly APIScore Score;
+ protected readonly SoloScoreInfo Score;
[Resolved]
private OsuColour colours { get; set; }
@@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
- public DrawableProfileScore(APIScore score)
+ public DrawableProfileScore(SoloScoreInfo score)
{
Score = score;
@@ -98,7 +98,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Colour = colours.Yellow
},
- new DrawableDate(Score.Date, 12)
+ new DrawableDate(Score.EndedAt, 12)
{
Colour = colourProvider.Foreground1
}
@@ -138,7 +138,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
var ruleset = rulesets.GetRuleset(Score.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {Score.RulesetID} not found locally");
- return new ModIcon(ruleset.CreateInstance().CreateModFromAcronym(mod.Acronym))
+ return new ModIcon(mod.ToMod(ruleset.CreateInstance()))
{
Scale = new Vector2(0.35f)
};
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
index f8f83883fc..94d95dc27e 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
private readonly double weight;
- public DrawableProfileWeightedScore(APIScore score, double weight)
+ public DrawableProfileWeightedScore(SoloScoreInfo score, double weight)
: base(score)
{
this.weight = weight;
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
index 15c7b8f042..2564692c87 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs
@@ -17,7 +17,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
- public class PaginatedScoreContainer : PaginatedProfileSubsection
+ public class PaginatedScoreContainer : PaginatedProfileSubsection
{
private readonly ScoreType type;
@@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
}
}
- protected override void OnItemsReceived(List items)
+ protected override void OnItemsReceived(List items)
{
if (CurrentPage == null || CurrentPage?.Offset == 0)
drawableItemIndex = 0;
@@ -62,12 +62,12 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
base.OnItemsReceived(items);
}
- protected override APIRequest> CreateRequest(PaginationParameters pagination) =>
+ protected override APIRequest> CreateRequest(PaginationParameters pagination) =>
new GetUserScoresRequest(User.Value.Id, type, pagination);
private int drawableItemIndex;
- protected override Drawable CreateDrawableItem(APIScore model)
+ protected override Drawable CreateDrawableItem(SoloScoreInfo model)
{
switch (type)
{
diff --git a/osu.Game/Overlays/Rankings/CountryFilter.cs b/osu.Game/Overlays/Rankings/CountryFilter.cs
index 9ba2018522..9376bafdff 100644
--- a/osu.Game/Overlays/Rankings/CountryFilter.cs
+++ b/osu.Game/Overlays/Rankings/CountryFilter.cs
@@ -16,14 +16,14 @@ using osuTK;
namespace osu.Game.Overlays.Rankings
{
- public class CountryFilter : CompositeDrawable, IHasCurrentValue
+ public class CountryFilter : CompositeDrawable, IHasCurrentValue
{
private const int duration = 200;
private const int height = 70;
- private readonly BindableWithCurrent current = new BindableWithCurrent();
+ private readonly BindableWithCurrent current = new BindableWithCurrent();
- public Bindable Current
+ public Bindable Current
{
get => current.Current;
set => current.Current = value;
@@ -89,9 +89,9 @@ namespace osu.Game.Overlays.Rankings
Current.BindValueChanged(onCountryChanged, true);
}
- private void onCountryChanged(ValueChangedEvent country)
+ private void onCountryChanged(ValueChangedEvent country)
{
- if (country.NewValue == null)
+ if (Current.Value == CountryCode.Unknown)
{
countryPill.Collapse();
this.ResizeHeightTo(0, duration, Easing.OutQuint);
diff --git a/osu.Game/Overlays/Rankings/CountryPill.cs b/osu.Game/Overlays/Rankings/CountryPill.cs
index 90f8c85557..ee4c4f08af 100644
--- a/osu.Game/Overlays/Rankings/CountryPill.cs
+++ b/osu.Game/Overlays/Rankings/CountryPill.cs
@@ -6,6 +6,7 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -21,13 +22,13 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.Rankings
{
- public class CountryPill : CompositeDrawable, IHasCurrentValue
+ public class CountryPill : CompositeDrawable, IHasCurrentValue
{
private const int duration = 200;
- private readonly BindableWithCurrent current = new BindableWithCurrent();
+ private readonly BindableWithCurrent current = new BindableWithCurrent();
- public Bindable Current
+ public Bindable Current
{
get => current.Current;
set => current.Current = value;
@@ -93,7 +94,7 @@ namespace osu.Game.Overlays.Rankings
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Action = () => Current.Value = null
+ Action = Current.SetDefault,
}
}
}
@@ -130,13 +131,13 @@ namespace osu.Game.Overlays.Rankings
this.FadeOut(duration, Easing.OutQuint);
}
- private void onCountryChanged(ValueChangedEvent country)
+ private void onCountryChanged(ValueChangedEvent country)
{
- if (country.NewValue == null)
+ if (Current.Value == CountryCode.Unknown)
return;
- flag.Country = country.NewValue;
- countryName.Text = country.NewValue.FullName;
+ flag.CountryCode = country.NewValue;
+ countryName.Text = country.NewValue.GetDescription();
}
private class CloseButton : OsuHoverContainer
diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs
index 9b0b75f663..936545bd49 100644
--- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs
+++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Rankings
{
public Bindable Ruleset => rulesetSelector.Current;
- public Bindable Country => countryFilter.Current;
+ public Bindable Country => countryFilter.Current;
private OverlayRulesetSelector rulesetSelector;
private CountryFilter countryFilter;
diff --git a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs
index 53d10b3e53..a6f0c7a123 100644
--- a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs
@@ -11,6 +11,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using System.Collections.Generic;
using osu.Framework.Allocation;
+using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Game.Resources.Localisation.Web;
@@ -33,9 +34,9 @@ namespace osu.Game.Overlays.Rankings.Tables
new RankingsTableColumn(RankingsStrings.StatAveragePerformance, Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
};
- protected override Country GetCountry(CountryStatistics item) => item.Country;
+ protected override CountryCode GetCountryCode(CountryStatistics item) => item.Code;
- protected override Drawable CreateFlagContent(CountryStatistics item) => new CountryName(item.Country);
+ protected override Drawable CreateFlagContent(CountryStatistics item) => new CountryName(item.Code);
protected override Drawable[] CreateAdditionalContent(CountryStatistics item) => new Drawable[]
{
@@ -70,15 +71,15 @@ namespace osu.Game.Overlays.Rankings.Tables
[Resolved(canBeNull: true)]
private RankingsOverlay rankings { get; set; }
- public CountryName(Country country)
+ public CountryName(CountryCode countryCode)
: base(t => t.Font = OsuFont.GetFont(size: 12))
{
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
TextAnchor = Anchor.CentreLeft;
- if (!string.IsNullOrEmpty(country.FullName))
- AddLink(country.FullName, () => rankings?.ShowCountry(country));
+ if (countryCode != CountryCode.Unknown)
+ AddLink(countryCode.GetDescription(), () => rankings?.ShowCountry(countryCode));
}
}
}
diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
index 56e318637a..073bf86a7a 100644
--- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Rankings.Tables
protected sealed override Drawable CreateHeader(int index, TableColumn column)
=> (column as RankingsTableColumn)?.CreateHeaderText() ?? new HeaderText(column?.Header ?? default, false);
- protected abstract Country GetCountry(TModel item);
+ protected abstract CountryCode GetCountryCode(TModel item);
protected abstract Drawable CreateFlagContent(TModel item);
@@ -97,10 +97,10 @@ namespace osu.Game.Overlays.Rankings.Tables
Margin = new MarginPadding { Bottom = row_spacing },
Children = new[]
{
- new UpdateableFlag(GetCountry(item))
+ new UpdateableFlag(GetCountryCode(item))
{
Size = new Vector2(28, 20),
- ShowPlaceholderOnNull = false,
+ ShowPlaceholderOnUnknown = false,
},
CreateFlagContent(item)
}
diff --git a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
index 2edd1b21ba..48185e6083 100644
--- a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Rankings.Tables
.Concat(GradeColumns.Select(grade => new GradeTableColumn(grade, Anchor.Centre, new Dimension(GridSizeMode.AutoSize))))
.ToArray();
- protected sealed override Country GetCountry(UserStatistics item) => item.User.Country;
+ protected sealed override CountryCode GetCountryCode(UserStatistics item) => item.User.CountryCode;
protected sealed override Drawable CreateFlagContent(UserStatistics item)
{
diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs
index a66b8cf2ba..586b883604 100644
--- a/osu.Game/Overlays/RankingsOverlay.cs
+++ b/osu.Game/Overlays/RankingsOverlay.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Overlays
{
public class RankingsOverlay : TabbableOnlineOverlay
{
- protected Bindable