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 d5390e6a3d..c83b7872ac 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 cebbcb40b7..19cf7f5d46 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -37,9 +37,15 @@ namespace osu.Desktop
// 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.\nPlease upgrade your operating system or consider using an older version of osu!.", IntPtr.Zero);
+ "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;
}
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/Mods/CatchModDoubleTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
index 4db0a53005..c58ce9b07d 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModDoubleTime.cs
@@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => 1.06;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
index e05932ca03..d166646eaf 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModFlashlight : ModFlashlight
{
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
index 7db9bf2dfd..39b992b3f5 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
{
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
{
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
index c02eedf936..b4fbc9d566 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset
{
public override string Description => @"Play with fading fruits.";
- public override double ScoreMultiplier => 1.06;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
private const double fade_out_offset_multiplier = 0.6;
private const double fade_out_duration_multiplier = 0.44;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
index 365d987794..1fd2227eb7 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs
@@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNightcore : ModNightcore
{
- public override double ScoreMultiplier => 1.06;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
}
}
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/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index bb593c2fb3..46f7c461f8 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.6972307565739273d, 206, "diffcalc-test")]
- [TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
+ [TestCase(6.6369583000323935d, 206, "diffcalc-test")]
+ [TestCase(1.4476531024675374d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
- [TestCase(8.9382559208689809d, 206, "diffcalc-test")]
- [TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
+ [TestCase(8.8816128335486386d, 206, "diffcalc-test")]
+ [TestCase(1.7540389962596916d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
- [TestCase(6.6972307218715166d, 239, "diffcalc-test")]
- [TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
+ [TestCase(6.6369583000323935d, 239, "diffcalc-test")]
+ [TestCase(1.4476531024675374d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
index 0694746cbf..76d5ccf682 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
@@ -108,13 +108,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
- // Reward for % distance slowed down compared to previous, paying attention to not award overlap
- double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
- // do not award overlap
- * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
-
- // Choose the largest bonus, multiplied by ratio.
- velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
+ velocityChangeBonus = overlapVelocityBuff * distRatio;
// Penalize for rhythm changes.
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 139bfe7dd3..59be93530c 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
@@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public class SliderPlacementBlueprint : PlacementBlueprint
{
- public new Objects.Slider HitObject => (Objects.Slider)base.HitObject;
+ public new Slider HitObject => (Slider)base.HitObject;
private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece;
@@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private IDistanceSnapProvider snapProvider { get; set; }
public SliderPlacementBlueprint()
- : base(new Objects.Slider())
+ : base(new Slider())
{
RelativeSizeAxes = Axes.Both;
@@ -82,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial:
BeginPlacement();
- var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
+ var nearestDifficultyPoint = editorBeatmap.HitObjects
+ .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime)?
+ .DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index 199c735787..17a9a81de8 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.Adjust;
public override ModType Type => ModType.DifficultyIncrease;
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
private DrawableOsuBlinds blinds;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs
index 704b922ee5..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), typeof(OsuModSingleTap) }).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/OsuModDoubleTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
index 2d19305509..b86efe84ee 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDoubleTime.cs
@@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index c082805a0e..b72e6b4dcb 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModFlashlight : ModFlashlight, IApplicableToDrawableHitObject
{
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
private const double default_follow_delay = 120;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
index fdddfed4d5..1f25655c8c 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHardRock : ModHardRock, IApplicableToHitObject
{
- public override double ScoreMultiplier => 1.06;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 11ceb0f710..253eaf473b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public Bindable OnlyFadeApproachCircles { get; } = new BindableBool();
public override string Description => @"Play with no approach circles and fading circles/sliders.";
- public override double ScoreMultiplier => 1.06;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
index e9be56fcc5..b1fe066a1e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs
@@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNightcore : ModNightcore
{
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
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/OsuModSingleTap.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
index 051ceb968c..b170d30448 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSingleTap.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModSingleTap : InputBlockingMod
{
public override string Name => @"Single Tap";
- public override string Acronym => @"ST";
+ 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();
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/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
index 254e220996..b77d4addee 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics;
@@ -32,19 +34,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
protected override void OnTrackingChanged(ValueChangedEvent tracking)
{
- const float scale_duration = 300f;
- const float fade_duration = 300f;
+ Debug.Assert(ParentObject != null);
- this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint)
- .FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint);
+ const float duration = 300f;
+
+ if (ParentObject.Judged)
+ return;
+
+ if (tracking.NewValue)
+ {
+ if (Precision.AlmostEquals(0, Alpha))
+ this.ScaleTo(1);
+
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA, duration, Easing.OutQuint)
+ .FadeTo(1f, duration, Easing.OutQuint);
+ }
+ else
+ {
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.2f, duration / 2, Easing.OutQuint)
+ .FadeTo(0, duration / 2, Easing.OutQuint);
+ }
}
protected override void OnSliderEnd()
{
- const float fade_duration = 450f;
+ const float fade_duration = 300;
// intentionally pile on an extra FadeOut to make it happen much faster
- this.FadeOut(fade_duration / 4, Easing.Out);
+ this.ScaleTo(1, fade_duration, Easing.OutQuint);
+ this.FadeOut(fade_duration / 2, Easing.OutQuint);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayerLegacySkin.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayerLegacySkin.cs
new file mode 100644
index 0000000000..13df24c988
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayerLegacySkin.cs
@@ -0,0 +1,19 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Taiko.Mods;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneTaikoPlayerLegacySkin : LegacySkinPlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset)
+ {
+ SelectedMods.Value = new[] { new TaikoModClassic() };
+ return base.CreatePlayer(ruleset);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index a9cde62f44..2c2dbddf13 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -35,13 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
- double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
-
- if (score.Mods.Any(m => m is ModNoFail))
- multiplier *= 0.90;
+ double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (score.Mods.Any(m => m is ModHidden))
- multiplier *= 1.10;
+ multiplier *= 1.075;
+
+ if (score.Mods.Any(m => m is ModEasy))
+ multiplier *= 0.975;
double difficultyValue = computeDifficultyValue(score, taikoAttributes);
double accuracyValue = computeAccuracyValue(score, taikoAttributes);
@@ -61,12 +61,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{
- double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.175) - 4.0, 2.25) / 450.0;
+ double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus;
- difficultyValue *= Math.Pow(0.985, countMiss);
+ difficultyValue *= Math.Pow(0.986, countMiss);
+
+ if (score.Mods.Any(m => m is ModEasy))
+ difficultyValue *= 0.980;
if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025;
@@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModFlashlight))
difficultyValue *= 1.05 * lengthBonus;
- return difficultyValue * score.Accuracy;
+ return difficultyValue * Math.Pow(score.Accuracy, 1.5);
}
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@@ -82,10 +85,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0)
return 0;
- double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0;
+ double accuracyValue = Math.Pow(140.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 12.0) * 27;
- // Bonus for many objects - it's harder to keep good accuracy up for longer
- return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
+ double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
+ accuracyValue *= lengthBonus;
+
+ // Slight HDFL Bonus for accuracy.
+ if (score.Mods.Any(m => m is ModFlashlight) && score.Mods.Any(m => m is ModHidden))
+ accuracyValue *= 1.10 * lengthBonus;
+
+ return accuracyValue;
}
private int totalHits => countGreat + countOk + countMeh + countMiss;
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
index 6d1a18bb78..233179c9ec 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
+
+ var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
+ playfield.ClassicHitTargetPosition.Value = true;
}
public void Update(Playfield playfield)
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
index 01f1632ae2..b19c2eaccf 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDoubleTime.cs
@@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModDoubleTime : ModDoubleTime
{
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index 2f9cccfe86..fe02a6caf9 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModFlashlight : ModFlashlight
{
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs
index 7fcd925c04..7780936e7d 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs
@@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModHardRock : ModHardRock
{
- public override double ScoreMultiplier => 1.06;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
///
/// Multiplier factor added to the scrolling speed.
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
index e065bb43fd..fe3e5ca11c 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset
{
public override string Description => @"Beats fade out before you hit them!";
- public override double ScoreMultiplier => 1.06;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
///
/// How far away from the hit target should hitobjects start to fade out.
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
index f2a57ecf88..e02a16f62f 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs
@@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModNightcore : ModNightcore
{
- public override double ScoreMultiplier => 1.12;
+ public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
}
}
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.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
index f18d8f0537..408fb28e6a 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
@@ -22,13 +22,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
///
internal class LegacyInputDrum : Container
{
+ private Container content;
private LegacyHalfDrum left;
private LegacyHalfDrum right;
- private Container content;
public LegacyInputDrum()
{
- RelativeSizeAxes = Axes.Both;
+ RelativeSizeAxes = Axes.Y;
+ AutoSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 55e920ece2..3f4a3e79d2 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osuTK;
@@ -33,7 +34,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{
sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
- RelativeSizeAxes = Axes.Both;
+ AutoSizeAxes = Axes.X;
+ RelativeSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
@@ -41,12 +43,32 @@ namespace osu.Game.Rulesets.Taiko.UI
{
Children = new Drawable[]
{
- new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new DefaultInputDrum())
{
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ },
+ sampleTriggerSource
+ };
+ }
+
+ private class DefaultInputDrum : AspectContainer
+ {
+ public DefaultInputDrum()
+ {
+ RelativeSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
Scale = new Vector2(0.9f),
- Children = new Drawable[]
+ Children = new[]
{
new TaikoHalfDrum(false)
{
@@ -71,131 +93,130 @@ namespace osu.Game.Rulesets.Taiko.UI
CentreAction = TaikoAction.RightCentre
}
}
- }),
- sampleTriggerSource
- };
- }
-
- ///
- /// A half-drum. Contains one centre and one rim hit.
- ///
- private class TaikoHalfDrum : Container, IKeyBindingHandler
- {
- ///
- /// The key to be used for the rim of the half-drum.
- ///
- public TaikoAction RimAction;
-
- ///
- /// The key to be used for the centre of the half-drum.
- ///
- public TaikoAction CentreAction;
-
- private readonly Sprite rim;
- private readonly Sprite rimHit;
- private readonly Sprite centre;
- private readonly Sprite centreHit;
-
- [Resolved]
- private DrumSampleTriggerSource sampleTriggerSource { get; set; }
-
- public TaikoHalfDrum(bool flipped)
- {
- Masking = true;
-
- Children = new Drawable[]
- {
- rim = new Sprite
- {
- Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both
- },
- rimHit = new Sprite
- {
- Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- Blending = BlendingParameters.Additive,
- },
- centre = new Sprite
- {
- Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Size = new Vector2(0.7f)
- },
- centreHit = new Sprite
- {
- Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Size = new Vector2(0.7f),
- Alpha = 0,
- Blending = BlendingParameters.Additive
- }
};
}
- [BackgroundDependencyLoader]
- private void load(TextureStore textures, OsuColour colours)
+ ///
+ /// A half-drum. Contains one centre and one rim hit.
+ ///
+ private class TaikoHalfDrum : Container, IKeyBindingHandler
{
- rim.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer");
- rimHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer-hit");
- centre.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner");
- centreHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner-hit");
+ ///
+ /// The key to be used for the rim of the half-drum.
+ ///
+ public TaikoAction RimAction;
- rimHit.Colour = colours.Blue;
- centreHit.Colour = colours.Pink;
- }
+ ///
+ /// The key to be used for the centre of the half-drum.
+ ///
+ public TaikoAction CentreAction;
- public bool OnPressed(KeyBindingPressEvent e)
- {
- Drawable target = null;
- Drawable back = null;
+ private readonly Sprite rim;
+ private readonly Sprite rimHit;
+ private readonly Sprite centre;
+ private readonly Sprite centreHit;
- if (e.Action == CentreAction)
+ [Resolved]
+ private DrumSampleTriggerSource sampleTriggerSource { get; set; }
+
+ public TaikoHalfDrum(bool flipped)
{
- target = centreHit;
- back = centre;
+ Masking = true;
- sampleTriggerSource.Play(HitType.Centre);
- }
- else if (e.Action == RimAction)
- {
- target = rimHit;
- back = rim;
-
- sampleTriggerSource.Play(HitType.Rim);
+ Children = new Drawable[]
+ {
+ rim = new Sprite
+ {
+ Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both
+ },
+ rimHit = new Sprite
+ {
+ Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ Blending = BlendingParameters.Additive,
+ },
+ centre = new Sprite
+ {
+ Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.7f)
+ },
+ centreHit = new Sprite
+ {
+ Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.7f),
+ Alpha = 0,
+ Blending = BlendingParameters.Additive
+ }
+ };
}
- if (target != null)
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures, OsuColour colours)
{
- const float scale_amount = 0.05f;
- const float alpha_amount = 0.5f;
+ rim.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer");
+ rimHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer-hit");
+ centre.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner");
+ centreHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner-hit");
- const float down_time = 40;
- const float up_time = 1000;
-
- back.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint)
- .Then()
- .ScaleTo(1, up_time, Easing.OutQuint);
-
- target.Animate(
- t => t.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint),
- t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.OutQuint)
- ).Then(
- t => t.ScaleTo(1, up_time, Easing.OutQuint),
- t => t.FadeOut(up_time, Easing.OutQuint)
- );
+ rimHit.Colour = colours.Blue;
+ centreHit.Colour = colours.Pink;
}
- return false;
- }
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ Drawable target = null;
+ Drawable back = null;
- public void OnReleased(KeyBindingReleaseEvent e)
- {
+ if (e.Action == CentreAction)
+ {
+ target = centreHit;
+ back = centre;
+
+ sampleTriggerSource.Play(HitType.Centre);
+ }
+ else if (e.Action == RimAction)
+ {
+ target = rimHit;
+ back = rim;
+
+ sampleTriggerSource.Play(HitType.Rim);
+ }
+
+ if (target != null)
+ {
+ const float scale_amount = 0.05f;
+ const float alpha_amount = 0.5f;
+
+ const float down_time = 40;
+ const float up_time = 1000;
+
+ back.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint)
+ .Then()
+ .ScaleTo(1, up_time, Easing.OutQuint);
+
+ target.Animate(
+ t => t.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint),
+ t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.OutQuint)
+ ).Then(
+ t => t.ScaleTo(1, up_time, Easing.OutQuint),
+ t => t.FadeOut(up_time, Easing.OutQuint)
+ );
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 4ef7c24464..ec76125540 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -34,6 +35,11 @@ namespace osu.Game.Rulesets.Taiko.UI
///
public const float DEFAULT_HEIGHT = 200;
+ ///
+ /// Whether the hit target should be nudged further towards the left area, matching the stable "classic" position.
+ ///
+ public Bindable ClassicHitTargetPosition = new BindableBool();
+
private Container hitExplosionContainer;
private Container kiaiExplosionContainer;
private JudgementContainer judgementContainer;
@@ -45,8 +51,8 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly IDictionary explosionPools = new Dictionary();
private ProxyContainer topLevelHitContainer;
+ private InputDrum inputDrum;
private Container rightArea;
- private Container leftArea;
///
/// is purposefully not called on this to prevent i.e. being able to interact
@@ -54,14 +60,43 @@ namespace osu.Game.Rulesets.Taiko.UI
///
private BarLinePlayfield barLinePlayfield;
- private Container hitTargetOffsetContent;
+ private Container playfieldContent;
+ private Container playfieldOverlay;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
+ inputDrum = new InputDrum(HitObjectContainer)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.X,
+ RelativeSizeAxes = Axes.Y,
+ };
+
InternalChildren = new[]
{
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()),
+ new Container
+ {
+ Name = "Left overlay",
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ BorderColour = colours.Gray0,
+ Children = new[]
+ {
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
+ inputDrum.CreateProxy(),
+ }
+ },
+ mascot = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Mascot), _ => Empty())
+ {
+ Origin = Anchor.BottomLeft,
+ Anchor = Anchor.TopLeft,
+ RelativePositionAxes = Axes.Y,
+ RelativeSizeAxes = Axes.None,
+ Y = 0.2f
+ },
rightArea = new Container
{
Name = "Right area",
@@ -71,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
new Container
{
- Name = "Masked elements before hit objects",
+ Name = "Elements before hit objects",
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Children = new[]
@@ -86,22 +121,28 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
},
- hitTargetOffsetContent = new Container
+ new Container
{
+ Name = "Masked hit objects content",
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Child = playfieldContent = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ barLinePlayfield = new BarLinePlayfield(),
+ HitObjectContainer,
+ }
+ }
+ },
+ playfieldOverlay = new Container
+ {
+ Name = "Elements after hit objects",
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- barLinePlayfield = new BarLinePlayfield(),
- new Container
- {
- Name = "Hit objects",
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- HitObjectContainer,
- drumRollHitContainer = new DrumRollHitContainer()
- }
- },
+ drumRollHitContainer = new DrumRollHitContainer(),
kiaiExplosionContainer = new Container
{
Name = "Kiai hit explosions",
@@ -117,36 +158,15 @@ namespace osu.Game.Rulesets.Taiko.UI
},
}
},
- leftArea = new Container
- {
- Name = "Left overlay",
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
- BorderColour = colours.Gray0,
- Children = new Drawable[]
- {
- new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
- new InputDrum(HitObjectContainer)
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- },
- }
- },
- mascot = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.Mascot), _ => Empty())
- {
- Origin = Anchor.BottomLeft,
- Anchor = Anchor.TopLeft,
- RelativePositionAxes = Axes.Y,
- RelativeSizeAxes = Axes.None,
- Y = 0.2f
- },
topLevelHitContainer = new ProxyContainer
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
},
drumRollHitContainer.CreateProxy(),
+ // this is added at the end of the hierarchy to receive input before taiko objects.
+ // but is proxied below everything to not cover visual effects such as hit explosions.
+ inputDrum,
};
RegisterPool(50);
@@ -193,8 +213,9 @@ namespace osu.Game.Rulesets.Taiko.UI
// Padding is required to be updated for elements which are based on "absolute" X sized elements.
// This is basically allowing for correct alignment as relative pieces move around them.
- rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth };
- hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
+ rightArea.Padding = new MarginPadding { Left = inputDrum.Width };
+ playfieldContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
+ playfieldOverlay.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT);
}
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/Mods/ModDifficultyAdjustTest.cs b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
index a6f68b2836..4101652c49 100644
--- a/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
+++ b/osu.Game.Tests/Mods/ModDifficultyAdjustTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
@@ -17,7 +15,7 @@ namespace osu.Game.Tests.Mods
[TestFixture]
public class ModDifficultyAdjustTest
{
- private TestModDifficultyAdjust testMod;
+ private TestModDifficultyAdjust testMod = null!;
[SetUp]
public void Setup()
@@ -148,7 +146,7 @@ namespace osu.Game.Tests.Mods
yield return new TestModDifficultyAdjust();
}
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null)
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null)
{
throw new System.NotImplementedException();
}
diff --git a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
index e94ee40acd..cd6879cf01 100644
--- a/osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
+++ b/osu.Game.Tests/Mods/ModSettingsEqualityComparison.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 NUnit.Framework;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
diff --git a/osu.Game.Tests/Mods/ModSettingsTest.cs b/osu.Game.Tests/Mods/ModSettingsTest.cs
index 607b585d33..b9ea1f2567 100644
--- a/osu.Game.Tests/Mods/ModSettingsTest.cs
+++ b/osu.Game.Tests/Mods/ModSettingsTest.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 NUnit.Framework;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs
index 22be1a3f01..3b391f6756 100644
--- a/osu.Game.Tests/Mods/ModUtilsTest.cs
+++ b/osu.Game.Tests/Mods/ModUtilsTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using Moq;
@@ -164,19 +162,19 @@ namespace osu.Game.Tests.Mods
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
- null
+ Array.Empty()
},
// invalid free mod is valid for local.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
- null
+ Array.Empty()
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
- null
+ Array.Empty()
},
};
@@ -216,13 +214,13 @@ namespace osu.Game.Tests.Mods
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
- null
+ Array.Empty()
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
- null
+ Array.Empty()
},
};
@@ -256,19 +254,19 @@ namespace osu.Game.Tests.Mods
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
- null,
+ Array.Empty(),
},
// incompatible pair with derived class is valid for free mods.
new object[]
{
new Mod[] { new OsuModDeflate(), new OsuModSpinIn() },
- null,
+ Array.Empty(),
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
- null
+ Array.Empty()
},
};
@@ -277,12 +275,12 @@ namespace osu.Game.Tests.Mods
{
bool isValid = ModUtils.CheckValidForGameplay(inputMods, out var invalid);
- Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
+ Assert.That(isValid, Is.EqualTo(expectedInvalid.Length == 0));
if (isValid)
Assert.IsNull(invalid);
else
- Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
+ Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
[TestCaseSource(nameof(invalid_multiplayer_mod_test_scenarios))]
@@ -290,12 +288,12 @@ namespace osu.Game.Tests.Mods
{
bool isValid = ModUtils.CheckValidRequiredModsForMultiplayer(inputMods, out var invalid);
- Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
+ Assert.That(isValid, Is.EqualTo(expectedInvalid.Length == 0));
if (isValid)
Assert.IsNull(invalid);
else
- Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
+ Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
[TestCaseSource(nameof(invalid_free_mod_test_scenarios))]
@@ -303,12 +301,12 @@ namespace osu.Game.Tests.Mods
{
bool isValid = ModUtils.CheckValidFreeModsForMultiplayer(inputMods, out var invalid);
- Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
+ Assert.That(isValid, Is.EqualTo(expectedInvalid.Length == 0));
if (isValid)
Assert.IsNull(invalid);
else
- Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
+ Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
diff --git a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
index 3c69adcb59..b8a3828a64 100644
--- a/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
+++ b/osu.Game.Tests/Mods/MultiModIncompatibilityTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -29,10 +27,10 @@ namespace osu.Game.Tests.Mods
[TestCase(typeof(ManiaRuleset))]
public void TestAllMultiModsFromRulesetAreIncompatible(Type rulesetType)
{
- var ruleset = (Ruleset)Activator.CreateInstance(rulesetType);
+ var ruleset = Activator.CreateInstance(rulesetType) as Ruleset;
Assert.That(ruleset, Is.Not.Null);
- var allMultiMods = getMultiMods(ruleset);
+ var allMultiMods = getMultiMods(ruleset!);
Assert.Multiple(() =>
{
diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
index f608d020d4..dd105787fa 100644
--- a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
+++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
diff --git a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
index 08007503c6..9e3354935a 100644
--- a/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
+++ b/osu.Game.Tests/Mods/TestCustomisableModRuleset.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using osu.Framework.Bindables;
@@ -33,7 +31,7 @@ namespace osu.Game.Tests.Mods
return Array.Empty();
}
- public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new NotImplementedException();
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList? mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
index c887105da6..461102124a 100644
--- a/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
+++ b/osu.Game.Tests/NonVisual/BeatmapSetInfoEqualityTest.cs
@@ -62,9 +62,45 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
- private static void addAudioFile(BeatmapSetInfo beatmapSetInfo, string hash = null)
+ [Test]
+ public void TestAudioEqualityBeatmapInfoSameHash()
{
- beatmapSetInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = hash ?? Guid.NewGuid().ToString() }, "audio.mp3"));
+ 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]
diff --git a/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
index d0176da0e9..e7a6e9a543 100644
--- a/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
+++ b/osu.Game.Tests/Online/TestSceneBeatmapDownloading.cs
@@ -80,7 +80,7 @@ namespace osu.Game.Tests.Online
{
AddStep("download beatmap", () => beatmaps.Download(test_db_model));
- AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_db_model).Cancel());
+ AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_db_model)!.Cancel());
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index fcf69bf6f2..536322805b 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -126,10 +126,10 @@ namespace osu.Game.Tests.Online
AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet));
addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0f));
- AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)).SetProgress(0.4f));
+ AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.SetProgress(0.4f));
addAvailabilityCheckStep("state downloading 40%", () => BeatmapAvailability.Downloading(0.4f));
- AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)).TriggerSuccess(testBeatmapFile));
+ AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
@@ -246,7 +246,7 @@ namespace osu.Game.Tests.Online
=> new TestDownloadRequest(set);
}
- private class TestDownloadRequest : ArchiveDownloadRequest
+ internal class TestDownloadRequest : ArchiveDownloadRequest
{
public new void SetProgress(float progress) => base.SetProgress(progress);
public new void TriggerSuccess(string filename) => base.TriggerSuccess(filename);
diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs
index 41404b2636..ee29cc8644 100644
--- a/osu.Game.Tests/Resources/TestResources.cs
+++ b/osu.Game.Tests/Resources/TestResources.cs
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Resources
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/Rulesets/Mods/ModTimeRampTest.cs b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs
index 2622db464f..4601737558 100644
--- a/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs
+++ b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.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 NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Timing;
@@ -19,8 +17,8 @@ namespace osu.Game.Tests.Rulesets.Mods
private const double start_time = 1000;
private const double duration = 9000;
- private TrackVirtual track;
- private OsuPlayfield playfield;
+ private TrackVirtual track = null!;
+ private OsuPlayfield playfield = null!;
[SetUp]
public void SetUp()
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
index 2cada1989e..6c5cca1874 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineZoom.cs
@@ -5,7 +5,6 @@
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Framework.Utils;
namespace osu.Game.Tests.Visual.Editing
{
@@ -32,12 +31,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
AddStep("scale zoom", () => TimelineArea.Timeline.Zoom = 200);
- AddAssert("range halved", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange / 2, 1));
+ AddStep("range halved", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange / 2).Within(1)));
AddStep("descale zoom", () => TimelineArea.Timeline.Zoom = 50);
- AddAssert("range doubled", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange * 2, 1));
+ AddStep("range doubled", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange * 2).Within(1)));
AddStep("restore zoom", () => TimelineArea.Timeline.Zoom = 100);
- AddAssert("range restored", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange, 1));
+ AddStep("range restored", () => Assert.That(TimelineArea.Timeline.VisibleRange, Is.EqualTo(initialVisibleRange).Within(1)));
}
[Test]
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 5ec9e88728..6491987abe 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
@@ -8,14 +8,18 @@ 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;
@@ -58,14 +62,35 @@ 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);
- AddStep("set no custom ruleset", () => customRuleset = null);
AddAssert("last played is null", () => getLastPlayed() == null);
CreateTest();
@@ -77,8 +102,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[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/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index a2793acba7..d35887c443 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -402,16 +402,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect()
{
- createRoom(() => new Room
+ PlaylistItem? item = null;
+ createRoom(() =>
{
- Name = { Value = "Test Room" },
- Playlist =
+ item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
- new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
- {
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID
- }
- }
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID
+ };
+ return new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist = { item }
+ };
});
pressReadyButton();
@@ -419,7 +421,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
@@ -440,16 +442,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPlayStartsWithCorrectRulesetWhileAtSongSelect()
{
- createRoom(() => new Room
+ PlaylistItem? item = null;
+ createRoom(() =>
{
- Name = { Value = "Test Room" },
- Playlist =
+ item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
- new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
- {
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID
- }
- }
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID
+ };
+ return new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist = { item }
+ };
});
pressReadyButton();
@@ -457,7 +461,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
@@ -478,16 +482,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestPlayStartsWithCorrectModsWhileAtSongSelect()
{
- createRoom(() => new Room
+ PlaylistItem? item = null;
+ createRoom(() =>
{
- Name = { Value = "Test Room" },
- Playlist =
+ item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
- new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
- {
- RulesetID = new OsuRuleset().RulesetInfo.OnlineID
- }
- }
+ RulesetID = new OsuRuleset().RulesetInfo.OnlineID
+ };
+ return new Room
+ {
+ Name = { Value = "Test Room" },
+ Playlist = { item }
+ };
});
pressReadyButton();
@@ -495,7 +501,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen;
- ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(multiplayerClient.ClientRoom?.Settings.PlaylistItemId);
+ ((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(item);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType().FirstOrDefault()?.BeatmapSetsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
index 4c39dc34d5..c5ac3dd442 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
@@ -108,6 +108,7 @@ namespace osu.Game.Tests.Visual.Online
Version = "2018.712.0",
DisplayVersion = "2018.712.0",
UpdateStream = streams[OsuGameBase.CLIENT_STREAM_NAME],
+ CreatedAt = new DateTime(2018, 7, 12),
ChangelogEntries = new List
{
new APIChangelogEntry
@@ -171,6 +172,7 @@ namespace osu.Game.Tests.Visual.Online
{
Version = "2019.920.0",
DisplayVersion = "2019.920.0",
+ CreatedAt = new DateTime(2019, 9, 20),
UpdateStream = new APIUpdateStream
{
Name = "Test",
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 be03328caa..864b2b6878 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneScoresContainer.cs
@@ -157,11 +157,7 @@ namespace osu.Game.Tests.Visual.Online
{
Id = 6602580,
Username = @"waaiiru",
- Country = new Country
- {
- FullName = @"Spain",
- FlagName = @"ES",
- },
+ CountryCode = CountryCode.ES,
},
Mods = new[]
{
@@ -184,11 +180,7 @@ namespace osu.Game.Tests.Visual.Online
{
Id = 4608074,
Username = @"Skycries",
- Country = new Country
- {
- FullName = @"Brazil",
- FlagName = @"BR",
- },
+ CountryCode = CountryCode.BR,
},
Mods = new[]
{
@@ -210,11 +202,7 @@ namespace osu.Game.Tests.Visual.Online
{
Id = 1014222,
Username = @"eLy",
- Country = new Country
- {
- FullName = @"Japan",
- FlagName = @"JP",
- },
+ CountryCode = CountryCode.JP,
},
Mods = new[]
{
@@ -235,11 +223,7 @@ namespace osu.Game.Tests.Visual.Online
{
Id = 1541390,
Username = @"Toukai",
- Country = new Country
- {
- FullName = @"Canada",
- FlagName = @"CA",
- },
+ CountryCode = CountryCode.CA,
},
Mods = new[]
{
@@ -259,11 +243,7 @@ namespace osu.Game.Tests.Visual.Online
{
Id = 7151382,
Username = @"Mayuri Hana",
- Country = new Country
- {
- FullName = @"Thailand",
- FlagName = @"TH",
- },
+ CountryCode = CountryCode.TH,
},
Rank = ScoreRank.D,
PP = 160,
@@ -274,15 +254,26 @@ namespace osu.Game.Tests.Visual.Online
}
};
+ const int initial_great_count = 2000;
+ const int initial_tick_count = 100;
+
+ int greatCount = initial_great_count;
+ int tickCount = initial_tick_count;
+
foreach (var s in scores.Scores)
{
s.Statistics = new Dictionary
{
- { HitResult.Great, RNG.Next(2000) },
- { HitResult.Ok, RNG.Next(2000) },
- { HitResult.Meh, RNG.Next(2000) },
- { HitResult.Miss, RNG.Next(2000) }
+ { HitResult.Great, greatCount },
+ { HitResult.LargeTickHit, tickCount },
+ { HitResult.Ok, RNG.Next(100) },
+ { HitResult.Meh, RNG.Next(100) },
+ { HitResult.Miss, initial_great_count - greatCount },
+ { HitResult.LargeTickMiss, initial_tick_count - tickCount },
};
+
+ greatCount -= 100;
+ tickCount -= RNG.Next(1, 5);
}
return scores;
@@ -298,11 +289,7 @@ namespace osu.Game.Tests.Visual.Online
{
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/Online/TestSceneWikiOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
index 8889cb3e37..558bff2f3c 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneWikiOverlay.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using System.Net;
using NUnit.Framework;
using osu.Game.Online.API;
@@ -25,24 +26,40 @@ namespace osu.Game.Tests.Visual.Online
public void TestMainPage()
{
setUpWikiResponse(responseMainPage);
- AddStep("Show Main Page", () => wiki.Show());
+ AddStep("Show main page", () => wiki.Show());
}
[Test]
public void TestArticlePage()
{
setUpWikiResponse(responseArticlePage);
- AddStep("Show Article Page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
+ AddStep("Show article page", () => wiki.ShowPage("Article_styling_criteria/Formatting"));
+ }
+
+ [Test]
+ public void TestRedirection()
+ {
+ const string redirection_path = "Redirection_path_for_article";
+
+ setUpWikiResponse(responseArticlePage, redirection_path);
+ AddStep("Show article page", () => wiki.ShowPage(redirection_path));
+
+ AddUntilStep("Current page is article", () => wiki.Header.Current.Value == "Formatting");
+
+ setUpWikiResponse(responseArticleParentPage);
+ AddStep("Show parent page", () => wiki.Header.ShowParentPage?.Invoke());
+
+ AddUntilStep("Current page is parent", () => wiki.Header.Current.Value == "Article styling criteria");
}
[Test]
public void TestErrorPage()
{
- setUpWikiResponse(null, true);
+ setUpWikiResponse(responseArticlePage);
AddStep("Show Error Page", () => wiki.ShowPage("Error"));
}
- private void setUpWikiResponse(APIWikiPage r, bool isFailed = false)
+ private void setUpWikiResponse(APIWikiPage r, string redirectionPath = null)
=> AddStep("set up response", () =>
{
dummyAPI.HandleRequest = request =>
@@ -50,10 +67,13 @@ namespace osu.Game.Tests.Visual.Online
if (!(request is GetWikiRequest getWikiRequest))
return false;
- if (isFailed)
- getWikiRequest.TriggerFailure(new WebException());
- else
+ if (getWikiRequest.Path.Equals(r.Path, StringComparison.OrdinalIgnoreCase) ||
+ getWikiRequest.Path.Equals(redirectionPath, StringComparison.OrdinalIgnoreCase))
+ {
getWikiRequest.TriggerSuccess(r);
+ }
+ else
+ getWikiRequest.TriggerFailure(new WebException());
return true;
};
@@ -82,5 +102,17 @@ namespace osu.Game.Tests.Visual.Online
Markdown =
"# Formatting\n\n*For the writing standards, see: [Article style criteria/Writing](../Writing)*\n\n*Notice: This article uses [RFC 2119](https://tools.ietf.org/html/rfc2119 \"IETF Tools\") to describe requirement levels.*\n\n## Locales\n\nListed below are the properly-supported locales for the wiki:\n\n| File Name | Locale Name | Native Script |\n| :-- | :-- | :-- |\n| `en.md` | English | English |\n| `ar.md` | Arabic | اَلْعَرَبِيَّةُ |\n| `be.md` | Belarusian | Беларуская мова |\n| `bg.md` | Bulgarian | Български |\n| `cs.md` | Czech | Česky |\n| `da.md` | Danish | Dansk |\n| `de.md` | German | Deutsch |\n| `gr.md` | Greek | Ελληνικά |\n| `es.md` | Spanish | Español |\n| `fi.md` | Finnish | Suomi |\n| `fr.md` | French | Français |\n| `hu.md` | Hungarian | Magyar |\n| `id.md` | Indonesian | Bahasa Indonesia |\n| `it.md` | Italian | Italiano |\n| `ja.md` | Japanese | 日本語 |\n| `ko.md` | Korean | 한국어 |\n| `nl.md` | Dutch | Nederlands |\n| `no.md` | Norwegian | Norsk |\n| `pl.md` | Polish | Polski |\n| `pt.md` | Portuguese | Português |\n| `pt-br.md` | Brazilian Portuguese | Português (Brasil) |\n| `ro.md` | Romanian | Română |\n| `ru.md` | Russian | Русский |\n| `sk.md` | Slovak | Slovenčina |\n| `sv.md` | Swedish | Svenska |\n| `th.md` | Thai | ไทย |\n| `tr.md` | Turkish | Türkçe |\n| `uk.md` | Ukrainian | Українська мова |\n| `vi.md` | Vietnamese | Tiếng Việt |\n| `zh.md` | Chinese (Simplified) | 简体中文 |\n| `zh-tw.md` | Traditional Chinese (Taiwan) | 繁體中文(台灣) |\n\n*Note: The website will give readers their selected language's version of an article. If it is not available, the English version will be given.*\n\n### Content parity\n\nTranslations are subject to strict content parity with their English article, in the sense that they must have the same message, regardless of grammar and syntax. Any changes to the translations' meanings must be accompanied by equivalent changes to the English article.\n\nThere are some cases where the content is allowed to differ:\n\n- Articles originally written in a language other than English (in this case, English should act as the translation)\n- Explanations of English words that are common terms in the osu! community\n- External links\n- Tags\n- Subcommunity-specific explanations\n\n## Front matter\n\nFront matter must be placed at the very top of the file. It is written in [YAML](https://en.wikipedia.org/wiki/YAML#Example \"YAML Wikipedia article\") and describes additional information about the article. This must be surrounded by three hyphens (`---`) on the lines above and below it, and an empty line must follow it before the title heading.\n\n### Articles that need help\n\n*Note: Avoid translating English articles with this tag. In addition to this, this tag should be added when the translation needs its own clean up.*\n\nThe `needs_cleanup` tag may be added to articles that need rewriting or formatting help. It is also acceptable to open an issue on GitHub for this purpose. This tag must be written as shown below:\n\n```yaml\nneeds_cleanup: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be done to remove the tag.\n\n### Outdated articles\n\n*Note: Avoid translating English articles with this tag. If the English article has this tag, the translation must also have this tag.*\n\nTranslated articles that are outdated must use the `outdated` tag when the English variant is updated. English articles may also become outdated when the content they contain is misleading or no longer relevant. This tag must be written as shown below:\n\n```yaml\noutdated: true\n```\n\nWhen adding this tag to an article, [comments](#comments) should also be added to explain what needs to be updated to remove the tag.\n\n### Tagging articles\n\nTags help the website's search engine query articles better. Tags should be written in the same language as the article and include the original list of tags. Tags should use lowercase letters where applicable.\n\nFor example, an article called \"Beatmap discussion\" may include the following tags:\n\n```yaml\ntags:\n - beatmap discussions\n - modding V2\n - MV2\n```\n\n### Translations without reviews\n\n*Note: Wiki maintainers will determine and apply this mark prior to merging.*\n\nSometimes, translations are added to the wiki without review from other native speakers of the language. In this case, the `no_native_review` mark is added to let future translators know that it may need to be checked again. This tag must be written as shown below:\n\n```yaml\nno_native_review: true\n```\n\n## Article naming\n\n*See also: [Folder names](#folder-names) and [Titles](#titles)*\n\nArticle titles should be singular and use sentence case. See [Wikipedia's naming conventions article](https://en.wikipedia.org/wiki/Wikipedia:Naming_conventions_(plurals) \"Wikipedia\") for more details.\n\nArticle titles should match the folder name it is in (spaces may replace underscores (`_`) where appropriate). If the folder name changes, the article title should be changed to match it and vice versa.\n\n---\n\nContest and tournament articles are an exception. The folder name must use abbreviations, acronyms, or initialisms. The article's title must be the full name of the contest or tournament.\n\n## Folder and file structure\n\n### Folder names\n\n*See also: [Article naming](#article-naming)*\n\nFolder names must be in English and use sentence case.\n\nFolder names must only use these characters:\n\n- uppercase and lowercase letters\n- numbers\n- underscores (`_`)\n- hyphens (`-`)\n- exclamation marks (`!`)\n\n### Article file names\n\nThe file name of an article can be found in the `File Name` column of the [locales section](#locales). The location of a translated article must be placed in the same folder as the English article.\n\n### Index articles\n\nAn index article must be created if the folder is intended to only hold other articles. Index articles must contain a list of articles that are inside its own folder. They may also contain other information, such as a lead paragraph or descriptions of the linked articles.\n\n### Disambiguation articles\n\n[Disambiguation](/wiki/Disambiguation) articles must be placed in the `/wiki/Disambiguation` folder. The main page must be updated to include the disambiguation article. Refer to [Disambiguation/Mod](/wiki/Disambiguation/Mod) as an example.\n\nRedirects must be updated to have the ambiguous keyword(s) redirect to the disambiguation article.\n\nArticles linked from a disambiguation article must have a [For other uses](#for-other-uses) hatnote.\n\n## HTML\n\nHTML must not be used, with exception for [comments](#comments). The structure of the article must be redone if HTML is used.\n\n### Comments\n\nHTML comments should be used for marking to-dos, but may also be used to annotate text. They should be on their own line, but can be placed inline in a paragraph. If placed inline, the start of the comment must not have a space.\n\nBad example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\nGood example:\n\n```markdown\nHTML comments should be used for marking to-dos or annotate text.\n```\n\n## Editing\n\n### End of line sequence\n\n*Caution: Uploading Markdown files using `CRLF` (carriage return and line feed) via GitHub will result in those files using `CRLF`. To prevent this, set the line ending to `LF` (line feed) before uploading.*\n\nMarkdown files must be checked in using the `LF` end of line sequence.\n\n### Escaping\n\nMarkdown syntax should be escaped as needed. However, article titles are parsed as plain text and so must not be escaped.\n\n### Paragraphs\n\nEach paragraph must be followed by one empty line.\n\n### Line breaks\n\nLine breaks must use a backslash (`\\`).\n\nLine breaks must be used sparingly.\n\n## Hatnote\n\n*Not to be confused with [Notice](#notice).*\n\nHatnotes are short notes placed at the top of an article or section to help readers navigate to related articles or inform them about related topics.\n\nHatnotes must be italicised and be placed immediately after the heading. If multiple hatnotes are used, they must be on the same paragraph separated with a line break.\n\n### Main page\n\n*Main page* hatnotes direct the reader to the main article of a topic. When this hatnote is used, it implies that the section it is on is a summary of what the linked page is about. This hatnote should have only one link. These must be formatted as follows:\n\n```markdown\n*Main page: {article}*\n\n*Main pages: {article} and {article}*\n```\n\n### See also\n\n*See also* hatnotes suggest to readers other points of interest from a given article or section. These must be formatted as follows:\n\n```markdown\n*See also: {article}*\n\n*See also: {article} and {article}*\n```\n\n### For see\n\n*For see* hatnotes are similar to *see also* hatnotes, but are generally more descriptive and direct. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*For {description}, see: {article}`*\n\n*For {description}, see: {article} and {article}`*\n```\n\n### Not to be confused with\n\n*Not to be confused with* hatnotes help distinguish ambiguous or misunderstood article titles or sections. This hatnote may use more than one link if necessary. These must be formatted as follows:\n\n```markdown\n*Not to be confused with {article}.*\n\n*Not to be confused with {article} or {article}.*\n```\n\n### For other uses\n\n*For other uses* hatnotes are similar to *not to be confused with* hatnotes, but links directly to the [disambiguation article](#disambiguation-articles). This hatnote must only link to the disambiguation article. These must be formatted as follows:\n\n```markdown\n*For other uses, see {disambiguation article}.*\n```\n\n## Notice\n\n*Not to be confused with [Hatnote](#hatnote).*\n\nA notice should be placed where appropriate in a section, but must start off the paragraph and use italics. Notices may contain bolding where appropriate, but should be kept to a minimum. Notices must be written as complete sentences. Thus, unlike most [hatnotes](#hatnotes), must use a full stop (`.`) or an exclamation mark (`!`) if appropriate. Anything within the same paragraph of a notice must also be italicised. These must be formatted as follows:\n\n```markdown\n*Note: {note}.*\n\n*Notice: {notice}.*\n\n*Caution: {caution}.*\n\n*Warning: {warning}.*\n```\n\n- `Note` should be used for factual or trivial details.\n- `Notice` should be used for reminders or to draw attention to something that the reader should be made aware of.\n- `Caution` should be used to warn the reader to avoid unintended consequences.\n- `Warning` should be used to warn the reader that action may be taken against them.\n\n## Emphasising\n\n### Bold\n\nBold must use double asterisks (`**`).\n\nLead paragraphs may bold the first occurrence of the article's title.\n\n### Italics\n\nItalics must use single asterisks (`*`).\n\nNames of work or video games should be italicised. osu!—the game—is exempt from this.\n\nThe first occurrence of an abbreviation, acronym, or initialism may be italicised.\n\nItalics may also be used to provide emphasis or help with readability.\n\n## Headings\n\nAll headings must use sentence case.\n\nHeadings must use the [ATX (hash) style](https://github.github.com/gfm/#atx-headings \"GitHub\") and must have an empty line before and after the heading. The title heading is an exception when it is on the first line. If this is the case, there only needs to be an empty line after the title heading.\n\nHeadings must not exceed a heading level of 5 and must not be used to style or format text.\n\n### Titles\n\n*See also: [Article naming](#article-naming)*\n\n*Caution: Titles are parsed as plain text; they must not be escaped.*\n\nThe first heading in all articles must be a level 1 heading, being the article's title. All headings afterwards must be [section headings](#sections). Titles must not contain formatting, links, or images.\n\nThe title heading must be on the first line, unless [front matter](#front-matter) is being used. If that is the case, the title heading must go after it and have an empty line before the title heading.\n\n### Sections\n\nSection headings must use levels 2 to 5. The section heading proceeding the [title heading](#titles) must be a level 2 heading. Unlike titles, section headings may have small image icons.\n\nSection headings must not skip a heading level (i.e. do not go from a level 2 heading to a level 4 heading) and must not contain formatting or links.\n\n*Notice: On the website, heading levels 4 and 5 will not appear in the table of contents. They cannot be linked to directly either.*\n\n## Lists\n\nLists should not go over 4 levels of indentation and should not have an empty line in between each item.\n\nFor nested lists, bullets or numbers must align with the item content of their parent lists.\n\nThe following example was done incorrectly (take note of the spacing before the bullet):\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\nThe following example was done correctly:\n\n```markdown\n1. Fly a kite\n - Don't fly a kite if it's raining\n```\n\n### Bulleted\n\nBulleted lists must use a hyphen (`-`). These must then be followed by one space. (Example shown below.)\n\n```markdown\n- osu!\n - Hit circle\n - Combo number\n - Approach circle\n - Slider\n - Hit circles\n - Slider body\n - Slider ticks\n - Spinner\n- osu!taiko\n```\n\n### Numbered\n\nThe numbers in a numbered list must be incremented to represent their step.\n\n```markdown\n1. Download the osu! installer.\n2. Run the installer.\n 1. To change the installation location, click the text underneath the progression bar.\n 2. The installer will prompt for a new location, choose the installation folder.\n3. osu! will start up once installation is complete.\n4. Sign in.\n```\n\n### Mixed\n\nCombining both bulleted and numbered lists should be done sparingly.\n\n```markdown\n1. Download a skin from the forums.\n2. Load the skin file into osu!.\n - If the file is a `.zip`, unzip it and move the contents into the `Skins/` folder (found in your osu! installation folder).\n - If the file is a `.osk`, open it on your desktop or drag-and-drop it into the game client.\n3. Open osu!, if it is not opened, and select the skin in the options.\n - This may have been completed if you opened the `.osk` file or drag-and-dropped it into the game client.\n```\n\n## Code\n\nThe markup for code is a grave mark (`` ` ``). To put grave marks in code, use double grave marks instead. If the grave mark is at the start or end, pad it with one space. (Example shown below.)\n\n```markdown\n`` ` ``\n`` `Space` ``\n```\n\n### Keyboard keys\n\n*Notice: When denoting the letter itself, and not the keyboard key, use quotation marks instead.*\n\nWhen representing keyboard keys, use capital letters for single characters and title case for modifiers. Use the plus symbol (`+`) (without code) to represent key combinations. (Example shown below.)\n\n```markdown\npippi is spelt with a lowercase \"p\" like peppy.\n\nPress `Ctrl` + `O` to open the open dialog.\n```\n\nWhen representing a space or the spacebar, use `` `Space` ``.\n\n### Button and menu text\n\nWhen copying the text from a menu or button, the letter casing should be copied as it appears. (Example shown below.)\n\n```markdown\nThe `osu!direct` button is visible in the main menu on the right side, if you have an active osu!supporter tag.\n```\n\n### Folder and directory names\n\nWhen copying the name of a folder or directory, the letter casing should be copied as it appears, but prefer lowercased paths when possible. Directory paths must not be absolute (i.e. do not start the directory name from the drive letter or from the root folder). (Example shown below.)\n\n```markdown\nosu! is installed in the `AppData/Local` folder by default, unless specified otherwise during installation.\n```\n\n### Keywords and commands\n\nWhen copying a keyword or command, the letter casing should be copied as it appears or how someone normally would type it. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nAs of now, the `Name` and `Author` commands in the skin configuration file (`skin.ini`) do nothing.\n```\n\n### File names\n\nWhen copying the name of a file, the letter casing should be copied as it appears. If applicable, prefer lowercase letters. (Example shown below.)\n\n```markdown\nTo play osu!, double click the `osu!.exe` icon.\n```\n\n### File extensions\n\n*Notice: File formats (not to be confused with file extensions) must be written in capital letters without the prefixed fullstop (`.`).*\n\nFile extensions must be prefixed with a fullstop (`.`) and be followed by the file extension in lowercase letters. (Example shown below.)\n\n```markdown\nThe JPG (or JPEG) file format has the `.jpg` (or `.jpeg`) extension.\n```\n\n### Chat channels\n\nWhen copying the name of a chat channel, start it with a hash (`#`), followed by the channel name in lowercase letters. (Example shown below.)\n\n```markdown\n`#lobby` is where you can advertise your multi room.\n```\n\n## Preformatted text (code blocks)\n\n*Notice: Syntax highlighting for preformatted text is not implemented on the website yet.*\n\nPreformatted text (also known as code blocks) must be fenced using three grave marks. They should set the language identifier for syntax highlighting.\n\n## Links\n\nThere are two types of links: inline and reference. Inline has two styles.\n\nThe following is an example of both inline styles:\n\n```markdown\n[Game Modifiers](/wiki/Game_Modifiers)\n\n\n```\n\nThe following is an example of the reference style:\n\n```markdown\n[Game Modifiers][game mods link]\n\n[game mods link]: /wiki/Game_Modifiers\n```\n\n---\n\nLinks must use the inline style if they are only referenced once. The inline angle brackets style should be avoided. References to reference links must be placed at the bottom of the article.\n\n### Internal links\n\n*Note: Internal links refer to links that stay inside the `https://osu.ppy.sh/` domain.*\n\n#### Wiki links\n\nAll links that point to an wiki article should start with `/wiki/` followed by the path to get to the article you are targeting. Relative links may also be used. Some examples include the following:\n\n```markdown\n[FAQ](/wiki/FAQ)\n[pippi](/wiki/Mascots#-pippi)\n[Beatmaps](../)\n[Pattern](./Pattern)\n```\n\nWiki links must not use redirects and must not have a trailing forward slash (`/`).\n\nBad examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/ASC)\n[Developers](/wiki/Developers/)\n[Developers](/wiki/Developers/#game-client-developers)\n```\n\nGood examples include the following:\n\n```markdown\n[Article styling criteria](/wiki/Article_styling_criteria)\n[Developers](/wiki/Developers)\n[Developers](/wiki/Developers#game-client-developers)\n```\n\n##### Sub-article links\n\nWiki links that point to a sub-article should include the parent article's folder name in its link text. See the following example:\n\n```markdown\n*See also: [Beatmap Editor/Design](/wiki/Beatmap_Editor/Design)*\n```\n\n##### Section links\n\n*Notice: On the website, heading levels 4 and 5 are not given the id attribute. This means that they can not be linked to directly.*\n\nWiki links that point to a section of an article may use the section sign symbol (`§`). See the following example:\n\n```markdown\n*For timing rules, see: [Ranking Criteria § Timing](/wiki/Ranking_Criteria#timing)*\n```\n\n#### Other osu! links\n\nThe URL from the address bar of your web browser should be copied as it is when linking to other osu! web pages. The `https://osu.ppy.sh` part of the URL must be kept.\n\n##### User profiles\n\nAll usernames must be linked on first occurrence. Other occurrences are optional, but must be consistent throughout the entire article for all usernames. If it is difficult to determine the user's id, it may be skipped over.\n\nWhen linking to a user profile, the user's id number must be used. Use the new website (`https://osu.ppy.sh/users/{username})`) to get the user's id.\n\nThe link text of the user link should be the user's current name.\n\n##### Difficulties\n\nWhenever linking to a single difficulty, use this format as the link text:\n\n```\n{artist} - {title} ({creator}) [{difficuty_name}]\n```\n\nThe link must actually link to that difficulty. Beatmap difficulty URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}#{mode}/{BeatmapID}\n```\n\nThe difficulty name may be left outside of the link text, but doing so must be consistent throughout the entire article.\n\n##### Beatmaps\n\nWhenever linking to a beatmap, use this format as the link text:\n\n```\n{artist} - {title} ({creator})\n```\n\nAll beatmap URLs must be formatted as follows:\n\n```\nhttps://osu.ppy.sh/beatmapsets/{BeatmapSetID}\n```\n\n### External links\n\n*Notice: External links refers to links that go outside the `https://osu.ppy.sh/` domain.*\n\nThe `https` protocol must be used, unless the site does not support it. External links must be a clean and direct link to a reputable source. The link text should be the title of the page it is linking to. The URL from the address bar of your web browser should be copied as it is when linking to other external pages.\n\nThere are no visual differences between external and osu! web links. Due to this, the website name should be included in the title text. See the following example:\n\n```markdown\n*For more information about music theory, see: [Music theory](https://en.wikipedia.org/wiki/Music_theory \"Wikipedia\")*\n```\n\n## Images\n\nThere are two types of image links: inline and reference. Examples:\n\n**Inline style:**\n\n```markdown\n![](/wiki/shared/flag/AU.gif)\n```\n\n**Reference style:**\n\n```markdown\n![][flag_AU]\n\n[flag_AU]: /wiki/shared/flag/AU.gif\n```\n\nImages should use the inline linking style. References to reference links must be placed at the bottom of the article.\n\nImages must be placed in a folder named `img`, located in the article's folder. Images that are used in multiple articles should be stored in the `/wiki/shared/` folder.\n\n### Image caching\n\nImages on the website are cached for up to 60 days. The cached image is matched with the image link's URL.\n\nWhen updating an image, either change the image's name or append a query string to the URL. In both cases, all translations linking to the updated image should also be updated.\n\n### Formats and quality\n\nImages should use the JPG format at quality 8 (80 or 80%, depending on the program). If the image contains transparency or has text that must be readable, use the PNG format instead. If the image contains an animation, the GIF format can be used; however, this should be used sparingly as these may take longer to load or can be bigger then the [max file size](#file-size).\n\n### File size\n\nImages must be under 1 megabyte, otherwise they will fail to load. Downscaling and using JPG at 80% is almost always under the size limit.\n\nAll images should be optimised as much as possible. Use [jpeg-archive](https://github.com/danielgtaylor/jpeg-archive \"GitHub\") to compress JPEG images. For consistency, use the following command for jpeg-archive:\n\n```sh\njpeg-recompress -am smallfry