1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 14:47:25 +08:00

Merge remote-tracking branch 'ppy/master' into colour-encoding-2

This commit is contained in:
vun 2022-07-22 16:31:28 +08:00
commit 17d418d319
260 changed files with 4390 additions and 2404 deletions

View File

@ -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.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.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: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.

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.716.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.715.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.720.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -0,0 +1,76 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<bool> enabled = new BindableBool(true);
private SettingsSlider<float> deadzoneSlider = null!;
private Bindable<float> handlerDeadzone = null!;
private Bindable<float> 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<float>
{
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);
}
}
}

View File

@ -96,6 +96,9 @@ namespace osu.Android
case AndroidMouseHandler mh: case AndroidMouseHandler mh:
return new AndroidMouseSettings(mh); return new AndroidMouseSettings(mh);
case AndroidJoystickHandler jh:
return new AndroidJoystickSettings(jh);
default: default:
return base.CreateSettingsSubsectionFor(handler); return base.CreateSettingsSubsectionFor(handler);
} }

View File

@ -26,6 +26,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AndroidJoystickSettings.cs" />
<Compile Include="AndroidMouseSettings.cs" /> <Compile Include="AndroidMouseSettings.cs" />
<Compile Include="GameplayScreenRotationLocker.cs" /> <Compile Include="GameplayScreenRotationLocker.cs" />
<Compile Include="OsuGameActivity.cs" /> <Compile Include="OsuGameActivity.cs" />

View File

@ -22,10 +22,12 @@ using osu.Framework.Input.Handlers;
using osu.Framework.Input.Handlers.Joystick; using osu.Framework.Input.Handlers.Joystick;
using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Overlays.Settings.Sections.Input;
namespace osu.Desktop namespace osu.Desktop
@ -156,6 +158,9 @@ namespace osu.Desktop
case JoystickHandler jh: case JoystickHandler jh:
return new JoystickSettings(jh); return new JoystickSettings(jh);
case TouchHandler th:
return new InputSection.HandlerSection(th);
default: default:
return base.CreateSettingsSubsectionFor(handler); return base.CreateSettingsSubsectionFor(handler);
} }

View File

@ -37,9 +37,15 @@ namespace osu.Desktop
// See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms // See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)) 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, SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!", "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; return;
} }

View File

@ -0,0 +1,166 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}
}

View File

@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModDoubleTime : ModDoubleTime public class CatchModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
} }
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModFlashlight : ModFlashlight<CatchHitObject> public class CatchModFlashlight : ModFlashlight<CatchHitObject>
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat public override BindableFloat SizeMultiplier { get; } = new BindableFloat

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor) public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
{ {

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject> public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset<CatchHitObject>
{ {
public override string Description => @"Play with fading fruits."; 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_offset_multiplier = 0.6;
private const double fade_out_duration_multiplier = 0.44; private const double fade_out_duration_multiplier = 0.44;

View File

@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModNightcore : ModNightcore<CatchHitObject> public class CatchModNightcore : ModNightcore<CatchHitObject>
{ {
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
} }
} }

View File

@ -19,7 +19,9 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
public const float OBJECT_RADIUS = 64; public const float OBJECT_RADIUS = 64;
public readonly Bindable<float> OriginalXBindable = new Bindable<float>(); private HitObjectProperty<float> originalX;
public Bindable<float> OriginalXBindable => originalX.Bindable;
/// <summary> /// <summary>
/// The horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>. /// The horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
@ -31,18 +33,20 @@ namespace osu.Game.Rulesets.Catch.Objects
[JsonIgnore] [JsonIgnore]
public float X public float X
{ {
set => OriginalXBindable.Value = value; set => originalX.Value = value;
} }
public readonly Bindable<float> XOffsetBindable = new Bindable<float>(); private HitObjectProperty<float> xOffset;
public Bindable<float> XOffsetBindable => xOffset.Bindable;
/// <summary> /// <summary>
/// A random offset applied to the horizontal position, set by the beatmap processing. /// A random offset applied to the horizontal position, set by the beatmap processing.
/// </summary> /// </summary>
public float XOffset public float XOffset
{ {
get => XOffsetBindable.Value; get => xOffset.Value;
set => XOffsetBindable.Value = value; set => xOffset.Value = value;
} }
/// <summary> /// <summary>
@ -54,8 +58,8 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </remarks> /// </remarks>
public float OriginalX public float OriginalX
{ {
get => OriginalXBindable.Value; get => originalX.Value;
set => OriginalXBindable.Value = value; set => originalX.Value = value;
} }
/// <summary> /// <summary>
@ -69,59 +73,71 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TimePreempt { get; set; } = 1000; public double TimePreempt { get; set; } = 1000;
public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>(); private HitObjectProperty<int> indexInBeatmap;
public Bindable<int> IndexInBeatmapBindable => indexInBeatmap.Bindable;
public int IndexInBeatmap public int IndexInBeatmap
{ {
get => IndexInBeatmapBindable.Value; get => indexInBeatmap.Value;
set => IndexInBeatmapBindable.Value = value; set => indexInBeatmap.Value = value;
} }
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
public int ComboOffset { get; set; } public int ComboOffset { get; set; }
public Bindable<int> IndexInCurrentComboBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> indexInCurrentCombo;
public Bindable<int> IndexInCurrentComboBindable => indexInCurrentCombo.Bindable;
public int IndexInCurrentCombo public int IndexInCurrentCombo
{ {
get => IndexInCurrentComboBindable.Value; get => indexInCurrentCombo.Value;
set => IndexInCurrentComboBindable.Value = value; set => indexInCurrentCombo.Value = value;
} }
public Bindable<int> ComboIndexBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> comboIndex;
public Bindable<int> ComboIndexBindable => comboIndex.Bindable;
public int ComboIndex public int ComboIndex
{ {
get => ComboIndexBindable.Value; get => comboIndex.Value;
set => ComboIndexBindable.Value = value; set => comboIndex.Value = value;
} }
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> comboIndexWithOffsets;
public Bindable<int> ComboIndexWithOffsetsBindable => comboIndexWithOffsets.Bindable;
public int ComboIndexWithOffsets public int ComboIndexWithOffsets
{ {
get => ComboIndexWithOffsetsBindable.Value; get => comboIndexWithOffsets.Value;
set => ComboIndexWithOffsetsBindable.Value = value; set => comboIndexWithOffsets.Value = value;
} }
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>(); private HitObjectProperty<bool> lastInCombo;
public Bindable<bool> LastInComboBindable => lastInCombo.Bindable;
/// <summary> /// <summary>
/// The next fruit starts a new combo. Used for explodey. /// The next fruit starts a new combo. Used for explodey.
/// </summary> /// </summary>
public virtual bool LastInCombo public virtual bool LastInCombo
{ {
get => LastInComboBindable.Value; get => lastInCombo.Value;
set => LastInComboBindable.Value = value; set => lastInCombo.Value = value;
} }
public readonly Bindable<float> ScaleBindable = new Bindable<float>(1); private HitObjectProperty<float> scale = new HitObjectProperty<float>(1);
public Bindable<float> ScaleBindable => scale.Bindable;
public float Scale public float Scale
{ {
get => ScaleBindable.Value; get => scale.Value;
set => ScaleBindable.Value = value; set => scale.Value = value;
} }
/// <summary> /// <summary>

View File

@ -5,6 +5,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -24,12 +25,14 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary> /// </summary>
public float DistanceToHyperDash { get; set; } public float DistanceToHyperDash { get; set; }
public readonly Bindable<bool> HyperDashBindable = new Bindable<bool>(); private HitObjectProperty<bool> hyperDash;
public Bindable<bool> HyperDashBindable => hyperDash.Bindable;
/// <summary> /// <summary>
/// Whether this fruit can initiate a hyperdash. /// Whether this fruit can initiate a hyperdash.
/// </summary> /// </summary>
public bool HyperDash => HyperDashBindable.Value; public bool HyperDash => hyperDash.Value;
private CatchHitObject hyperDashTarget; private CatchHitObject hyperDashTarget;

View File

@ -13,12 +13,14 @@ namespace osu.Game.Rulesets.Mania.Objects
{ {
public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition public abstract class ManiaHitObject : HitObject, IHasColumn, IHasXPosition
{ {
public readonly Bindable<int> ColumnBindable = new Bindable<int>(); private HitObjectProperty<int> column;
public Bindable<int> ColumnBindable => column.Bindable;
public virtual int Column public virtual int Column
{ {
get => ColumnBindable.Value; get => column.Value;
set => ColumnBindable.Value = value; set => column.Value = value;
} }
protected override HitWindows CreateHitWindows() => new ManiaHitWindows(); protected override HitWindows CreateHitWindows() => new ManiaHitWindows();

View File

@ -17,18 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6972307565739273d, 206, "diffcalc-test")] [TestCase(6.6369583000323935d, 206, "diffcalc-test")]
[TestCase(1.4484754139145539d, 45, "zero-length-sliders")] [TestCase(1.4476531024675374d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9382559208689809d, 206, "diffcalc-test")] [TestCase(8.8816128335486386d, 206, "diffcalc-test")]
[TestCase(1.7548875851757628d, 45, "zero-length-sliders")] [TestCase(1.7540389962596916d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6972307218715166d, 239, "diffcalc-test")] [TestCase(6.6369583000323935d, 239, "diffcalc-test")]
[TestCase(1.4484754139145537d, 54, "zero-length-sliders")] [TestCase(1.4476531024675374d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

View File

@ -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. // 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)); 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 velocityChangeBonus = overlapVelocityBuff * distRatio;
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;
// Penalize for rhythm changes. // Penalize for rhythm changes.
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -24,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
public class SliderPlacementBlueprint : PlacementBlueprint public class SliderPlacementBlueprint : PlacementBlueprint
{ {
public new Objects.Slider HitObject => (Objects.Slider)base.HitObject; public new Slider HitObject => (Slider)base.HitObject;
private SliderBodyPiece bodyPiece; private SliderBodyPiece bodyPiece;
private HitCirclePiece headCirclePiece; private HitCirclePiece headCirclePiece;
@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private IDistanceSnapProvider snapProvider { get; set; } private IDistanceSnapProvider snapProvider { get; set; }
public SliderPlacementBlueprint() public SliderPlacementBlueprint()
: base(new Objects.Slider()) : base(new Slider())
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -82,7 +83,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial: case SliderPlacementState.Initial:
BeginPlacement(); 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.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => FontAwesome.Solid.Adjust; public override IconUsage? Icon => FontAwesome.Solid.Adjust;
public override ModType Type => ModType.DifficultyIncrease; 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) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
private DrawableOsuBlinds blinds; private DrawableOsuBlinds blinds;

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModCinema : ModCinema<OsuHitObject> public class OsuModCinema : ModCinema<OsuHitObject>
{ {
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<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModDoubleTime : ModDoubleTime public class OsuModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject public class OsuModFlashlight : ModFlashlight<OsuHitObject>, 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(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
private const double default_follow_delay = 120; private const double default_follow_delay = 120;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModHardRock : ModHardRock, IApplicableToHitObject 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(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModMirror)).ToArray();

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool(); public Bindable<bool> OnlyFadeApproachCircles { get; } = new BindableBool();
public override string Description => @"Play with no approach circles and fading circles/sliders."; 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) }; public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };

View File

@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModNightcore : ModNightcore<OsuHitObject> public class OsuModNightcore : ModNightcore<OsuHitObject>
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -3,11 +3,14 @@
#nullable disable #nullable disable
using System;
using System.Linq;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModPerfect : ModPerfect public class OsuModPerfect : ModPerfect
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
} }
} }

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModSingleTap : InputBlockingMod public class OsuModSingleTap : InputBlockingMod
{ {
public override string Name => @"Single Tap"; 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 string Description => @"You must only use one key!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();

View File

@ -7,12 +7,12 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; 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.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
@ -36,12 +36,14 @@ namespace osu.Game.Rulesets.Osu.Objects
public double TimePreempt = 600; public double TimePreempt = 600;
public double TimeFadeIn = 400; public double TimeFadeIn = 400;
public readonly Bindable<Vector2> PositionBindable = new Bindable<Vector2>(); private HitObjectProperty<Vector2> position;
public Bindable<Vector2> PositionBindable => position.Bindable;
public virtual Vector2 Position public virtual Vector2 Position
{ {
get => PositionBindable.Value; get => position.Value;
set => PositionBindable.Value = value; set => position.Value = value;
} }
public float X => Position.X; public float X => Position.X;
@ -53,66 +55,80 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedEndPosition => EndPosition + StackOffset; public Vector2 StackedEndPosition => EndPosition + StackOffset;
public readonly Bindable<int> StackHeightBindable = new Bindable<int>(); private HitObjectProperty<int> stackHeight;
public Bindable<int> StackHeightBindable => stackHeight.Bindable;
public int StackHeight public int StackHeight
{ {
get => StackHeightBindable.Value; get => stackHeight.Value;
set => StackHeightBindable.Value = value; set => stackHeight.Value = value;
} }
public virtual Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f); public virtual Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f);
public double Radius => OBJECT_RADIUS * Scale; public double Radius => OBJECT_RADIUS * Scale;
public readonly Bindable<float> ScaleBindable = new BindableFloat(1); private HitObjectProperty<float> scale = new HitObjectProperty<float>(1);
public Bindable<float> ScaleBindable => scale.Bindable;
public float Scale public float Scale
{ {
get => ScaleBindable.Value; get => scale.Value;
set => ScaleBindable.Value = value; set => scale.Value = value;
} }
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
public readonly Bindable<int> ComboOffsetBindable = new Bindable<int>(); private HitObjectProperty<int> comboOffset;
public Bindable<int> ComboOffsetBindable => comboOffset.Bindable;
public int ComboOffset public int ComboOffset
{ {
get => ComboOffsetBindable.Value; get => comboOffset.Value;
set => ComboOffsetBindable.Value = value; set => comboOffset.Value = value;
} }
public Bindable<int> IndexInCurrentComboBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> indexInCurrentCombo;
public Bindable<int> IndexInCurrentComboBindable => indexInCurrentCombo.Bindable;
public virtual int IndexInCurrentCombo public virtual int IndexInCurrentCombo
{ {
get => IndexInCurrentComboBindable.Value; get => indexInCurrentCombo.Value;
set => IndexInCurrentComboBindable.Value = value; set => indexInCurrentCombo.Value = value;
} }
public Bindable<int> ComboIndexBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> comboIndex;
public Bindable<int> ComboIndexBindable => comboIndex.Bindable;
public virtual int ComboIndex public virtual int ComboIndex
{ {
get => ComboIndexBindable.Value; get => comboIndex.Value;
set => ComboIndexBindable.Value = value; set => comboIndex.Value = value;
} }
public Bindable<int> ComboIndexWithOffsetsBindable { get; } = new Bindable<int>(); private HitObjectProperty<int> comboIndexWithOffsets;
public Bindable<int> ComboIndexWithOffsetsBindable => comboIndexWithOffsets.Bindable;
public int ComboIndexWithOffsets public int ComboIndexWithOffsets
{ {
get => ComboIndexWithOffsetsBindable.Value; get => comboIndexWithOffsets.Value;
set => ComboIndexWithOffsetsBindable.Value = value; set => comboIndexWithOffsets.Value = value;
} }
public Bindable<bool> LastInComboBindable { get; } = new Bindable<bool>(); private HitObjectProperty<bool> lastInCombo;
public Bindable<bool> LastInComboBindable => lastInCombo.Bindable;
public bool LastInCombo public bool LastInCombo
{ {
get => LastInComboBindable.Value; get => lastInCombo.Value;
set => LastInComboBindable.Value = value; set => lastInCombo.Value = value;
} }
protected OsuHitObject() protected OsuHitObject()

View File

@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics; using osuTK.Graphics;
@ -32,19 +34,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
protected override void OnTrackingChanged(ValueChangedEvent<bool> tracking) protected override void OnTrackingChanged(ValueChangedEvent<bool> tracking)
{ {
const float scale_duration = 300f; Debug.Assert(ParentObject != null);
const float fade_duration = 300f;
this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint) const float duration = 300f;
.FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint);
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() 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 // 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);
} }
} }
} }

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -35,13 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); 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 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 ModNoFail))
multiplier *= 0.90;
if (score.Mods.Any(m => m is ModHidden)) 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 difficultyValue = computeDifficultyValue(score, taikoAttributes);
double accuracyValue = computeAccuracyValue(score, taikoAttributes); double accuracyValue = computeAccuracyValue(score, taikoAttributes);
@ -61,12 +61,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) 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); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus; 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)) if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025; difficultyValue *= 1.025;
@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>)) if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.05 * lengthBonus; difficultyValue *= 1.05 * lengthBonus;
return difficultyValue * score.Accuracy; return difficultyValue * Math.Pow(score.Accuracy, 1.5);
} }
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@ -82,10 +85,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0) if (attributes.GreatHitWindow <= 0)
return 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 double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
return accValue * 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<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden))
accuracyValue *= 1.10 * lengthBonus;
return accuracyValue;
} }
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;

View File

@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspect.Value = false; drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
} }
public void Update(Playfield playfield) public void Update(Playfield playfield)

View File

@ -9,6 +9,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModDoubleTime : ModDoubleTime public class TaikoModDoubleTime : ModDoubleTime
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModFlashlight : ModFlashlight<TaikoHitObject> public class TaikoModFlashlight : ModFlashlight<TaikoHitObject>
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")] [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableFloat SizeMultiplier { get; } = new BindableFloat public override BindableFloat SizeMultiplier { get; } = new BindableFloat

View File

@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModHardRock : ModHardRock public class TaikoModHardRock : ModHardRock
{ {
public override double ScoreMultiplier => 1.06; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
/// <summary> /// <summary>
/// Multiplier factor added to the scrolling speed. /// Multiplier factor added to the scrolling speed.

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset<TaikoHitObject> public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset<TaikoHitObject>
{ {
public override string Description => @"Beats fade out before you hit them!"; 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;
/// <summary> /// <summary>
/// How far away from the hit target should hitobjects start to fade out. /// How far away from the hit target should hitobjects start to fade out.

View File

@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModNightcore : ModNightcore<TaikoHitObject> public class TaikoModNightcore : ModNightcore<TaikoHitObject>
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
} }
} }

View File

@ -11,14 +11,16 @@ namespace osu.Game.Rulesets.Taiko.Objects
{ {
public class BarLine : TaikoHitObject, IBarLine public class BarLine : TaikoHitObject, IBarLine
{ {
private HitObjectProperty<bool> major;
public Bindable<bool> MajorBindable => major.Bindable;
public bool Major public bool Major
{ {
get => MajorBindable.Value; get => major.Value;
set => MajorBindable.Value = value; set => major.Value = value;
} }
public readonly Bindable<bool> MajorBindable = new BindableBool();
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();
} }
} }

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics; using osuTK.Graphics;
@ -14,19 +15,21 @@ namespace osu.Game.Rulesets.Taiko.Objects
{ {
public class Hit : TaikoStrongableHitObject, IHasDisplayColour public class Hit : TaikoStrongableHitObject, IHasDisplayColour
{ {
public readonly Bindable<HitType> TypeBindable = new Bindable<HitType>(); private HitObjectProperty<HitType> type;
public Bindable<Color4> DisplayColour { get; } = new Bindable<Color4>(COLOUR_CENTRE); public Bindable<HitType> TypeBindable => type.Bindable;
/// <summary> /// <summary>
/// The <see cref="HitType"/> that actuates this <see cref="Hit"/>. /// The <see cref="HitType"/> that actuates this <see cref="Hit"/>.
/// </summary> /// </summary>
public HitType Type public HitType Type
{ {
get => TypeBindable.Value; get => type.Value;
set => TypeBindable.Value = value; set => type.Value = value;
} }
public Bindable<Color4> DisplayColour { get; } = new Bindable<Color4>(COLOUR_CENTRE);
public static readonly Color4 COLOUR_CENTRE = Color4Extensions.FromHex(@"bb1177"); public static readonly Color4 COLOUR_CENTRE = Color4Extensions.FromHex(@"bb1177");
public static readonly Color4 COLOUR_RIM = Color4Extensions.FromHex(@"2299bb"); public static readonly Color4 COLOUR_RIM = Color4Extensions.FromHex(@"2299bb");

View File

@ -22,13 +22,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
/// </summary> /// </summary>
internal class LegacyInputDrum : Container internal class LegacyInputDrum : Container
{ {
private Container content;
private LegacyHalfDrum left; private LegacyHalfDrum left;
private LegacyHalfDrum right; private LegacyHalfDrum right;
private Container content;
public LegacyInputDrum() public LegacyInputDrum()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -33,7 +34,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer); sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
RelativeSizeAxes = Axes.Both; AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -41,12 +43,32 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
Children = new Drawable[] 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, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Scale = new Vector2(0.9f), Scale = new Vector2(0.9f),
Children = new Drawable[] Children = new[]
{ {
new TaikoHalfDrum(false) new TaikoHalfDrum(false)
{ {
@ -71,131 +93,130 @@ namespace osu.Game.Rulesets.Taiko.UI
CentreAction = TaikoAction.RightCentre CentreAction = TaikoAction.RightCentre
} }
} }
}),
sampleTriggerSource
};
}
/// <summary>
/// A half-drum. Contains one centre and one rim hit.
/// </summary>
private class TaikoHalfDrum : Container, IKeyBindingHandler<TaikoAction>
{
/// <summary>
/// The key to be used for the rim of the half-drum.
/// </summary>
public TaikoAction RimAction;
/// <summary>
/// The key to be used for the centre of the half-drum.
/// </summary>
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] /// <summary>
private void load(TextureStore textures, OsuColour colours) /// A half-drum. Contains one centre and one rim hit.
/// </summary>
private class TaikoHalfDrum : Container, IKeyBindingHandler<TaikoAction>
{ {
rim.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer"); /// <summary>
rimHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer-hit"); /// The key to be used for the rim of the half-drum.
centre.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner"); /// </summary>
centreHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner-hit"); public TaikoAction RimAction;
rimHit.Colour = colours.Blue; /// <summary>
centreHit.Colour = colours.Pink; /// The key to be used for the centre of the half-drum.
} /// </summary>
public TaikoAction CentreAction;
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e) private readonly Sprite rim;
{ private readonly Sprite rimHit;
Drawable target = null; private readonly Sprite centre;
Drawable back = null; private readonly Sprite centreHit;
if (e.Action == CentreAction) [Resolved]
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped)
{ {
target = centreHit; Masking = true;
back = centre;
sampleTriggerSource.Play(HitType.Centre); Children = new Drawable[]
} {
else if (e.Action == RimAction) rim = new Sprite
{ {
target = rimHit; Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
back = rim; Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both
sampleTriggerSource.Play(HitType.Rim); },
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; rim.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer");
const float alpha_amount = 0.5f; 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; rimHit.Colour = colours.Blue;
const float up_time = 1000; centreHit.Colour = colours.Pink;
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 bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
} {
Drawable target = null;
Drawable back = null;
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> 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<TaikoAction> e)
{
}
} }
} }
} }

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
@ -34,6 +35,11 @@ namespace osu.Game.Rulesets.Taiko.UI
/// </summary> /// </summary>
public const float DEFAULT_HEIGHT = 200; public const float DEFAULT_HEIGHT = 200;
/// <summary>
/// Whether the hit target should be nudged further towards the left area, matching the stable "classic" position.
/// </summary>
public Bindable<bool> ClassicHitTargetPosition = new BindableBool();
private Container<HitExplosion> hitExplosionContainer; private Container<HitExplosion> hitExplosionContainer;
private Container<KiaiHitExplosion> kiaiExplosionContainer; private Container<KiaiHitExplosion> kiaiExplosionContainer;
private JudgementContainer<DrawableTaikoJudgement> judgementContainer; private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
@ -45,8 +51,8 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly IDictionary<HitResult, HitExplosionPool> explosionPools = new Dictionary<HitResult, HitExplosionPool>(); private readonly IDictionary<HitResult, HitExplosionPool> explosionPools = new Dictionary<HitResult, HitExplosionPool>();
private ProxyContainer topLevelHitContainer; private ProxyContainer topLevelHitContainer;
private InputDrum inputDrum;
private Container rightArea; private Container rightArea;
private Container leftArea;
/// <remarks> /// <remarks>
/// <see cref="Playfield.AddNested"/> is purposefully not called on this to prevent i.e. being able to interact /// <see cref="Playfield.AddNested"/> is purposefully not called on this to prevent i.e. being able to interact
@ -54,14 +60,43 @@ namespace osu.Game.Rulesets.Taiko.UI
/// </remarks> /// </remarks>
private BarLinePlayfield barLinePlayfield; private BarLinePlayfield barLinePlayfield;
private Container hitTargetOffsetContent; private Container playfieldContent;
private Container playfieldOverlay;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
inputDrum = new InputDrum(HitObjectContainer)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
};
InternalChildren = new[] InternalChildren = new[]
{ {
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight()), 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 rightArea = new Container
{ {
Name = "Right area", Name = "Right area",
@ -71,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
new Container new Container
{ {
Name = "Masked elements before hit objects", Name = "Elements before hit objects",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
Children = new[] 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, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
barLinePlayfield = new BarLinePlayfield(), drumRollHitContainer = new DrumRollHitContainer(),
new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
HitObjectContainer,
drumRollHitContainer = new DrumRollHitContainer()
}
},
kiaiExplosionContainer = new Container<KiaiHitExplosion> kiaiExplosionContainer = new Container<KiaiHitExplosion>
{ {
Name = "Kiai hit explosions", 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 topLevelHitContainer = new ProxyContainer
{ {
Name = "Top level hit objects", Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
drumRollHitContainer.CreateProxy(), 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<Hit, DrawableHit>(50); RegisterPool<Hit, DrawableHit>(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. // 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. // This is basically allowing for correct alignment as relative pieces move around them.
rightArea.Padding = new MarginPadding { Left = leftArea.DrawWidth }; rightArea.Padding = new MarginPadding { Left = inputDrum.Width };
hitTargetOffsetContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 }; playfieldContent.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
playfieldOverlay.Padding = new MarginPadding { Left = HitTarget.DrawWidth / 2 };
mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT); mascot.Scale = new Vector2(DrawHeight / DEFAULT_HEIGHT);
} }

View File

@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -17,7 +15,7 @@ namespace osu.Game.Tests.Mods
[TestFixture] [TestFixture]
public class ModDifficultyAdjustTest public class ModDifficultyAdjustTest
{ {
private TestModDifficultyAdjust testMod; private TestModDifficultyAdjust testMod = null!;
[SetUp] [SetUp]
public void Setup() public void Setup()
@ -148,7 +146,7 @@ namespace osu.Game.Tests.Mods
yield return new TestModDifficultyAdjust(); yield return new TestModDifficultyAdjust();
} }
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using Moq; using Moq;
@ -164,19 +162,19 @@ namespace osu.Game.Tests.Mods
new object[] new object[]
{ {
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
null Array.Empty<Type>()
}, },
// invalid free mod is valid for local. // invalid free mod is valid for local.
new object[] new object[]
{ {
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
null Array.Empty<Type>()
}, },
// valid pair. // valid pair.
new object[] new object[]
{ {
new Mod[] { new OsuModHidden(), new OsuModHardRock() }, new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null Array.Empty<Type>()
}, },
}; };
@ -216,13 +214,13 @@ namespace osu.Game.Tests.Mods
new object[] new object[]
{ {
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
null Array.Empty<Type>()
}, },
// valid pair. // valid pair.
new object[] new object[]
{ {
new Mod[] { new OsuModHidden(), new OsuModHardRock() }, new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null Array.Empty<Type>()
}, },
}; };
@ -256,19 +254,19 @@ namespace osu.Game.Tests.Mods
new object[] new object[]
{ {
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
null, Array.Empty<Type>(),
}, },
// incompatible pair with derived class is valid for free mods. // incompatible pair with derived class is valid for free mods.
new object[] new object[]
{ {
new Mod[] { new OsuModDeflate(), new OsuModSpinIn() }, new Mod[] { new OsuModDeflate(), new OsuModSpinIn() },
null, Array.Empty<Type>(),
}, },
// valid pair. // valid pair.
new object[] new object[]
{ {
new Mod[] { new OsuModHidden(), new OsuModHardRock() }, new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null Array.Empty<Type>()
}, },
}; };
@ -277,12 +275,12 @@ namespace osu.Game.Tests.Mods
{ {
bool isValid = ModUtils.CheckValidForGameplay(inputMods, out var invalid); 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) if (isValid)
Assert.IsNull(invalid); Assert.IsNull(invalid);
else 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))] [TestCaseSource(nameof(invalid_multiplayer_mod_test_scenarios))]
@ -290,12 +288,12 @@ namespace osu.Game.Tests.Mods
{ {
bool isValid = ModUtils.CheckValidRequiredModsForMultiplayer(inputMods, out var invalid); 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) if (isValid)
Assert.IsNull(invalid); Assert.IsNull(invalid);
else 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))] [TestCaseSource(nameof(invalid_free_mod_test_scenarios))]
@ -303,12 +301,12 @@ namespace osu.Game.Tests.Mods
{ {
bool isValid = ModUtils.CheckValidFreeModsForMultiplayer(inputMods, out var invalid); 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) if (isValid)
Assert.IsNull(invalid); Assert.IsNull(invalid);
else 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 public abstract class CustomMod1 : Mod, IModCompatibilitySpecification

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -29,10 +27,10 @@ namespace osu.Game.Tests.Mods
[TestCase(typeof(ManiaRuleset))] [TestCase(typeof(ManiaRuleset))]
public void TestAllMultiModsFromRulesetAreIncompatible(Type rulesetType) public void TestAllMultiModsFromRulesetAreIncompatible(Type rulesetType)
{ {
var ruleset = (Ruleset)Activator.CreateInstance(rulesetType); var ruleset = Activator.CreateInstance(rulesetType) as Ruleset;
Assert.That(ruleset, Is.Not.Null); Assert.That(ruleset, Is.Not.Null);
var allMultiMods = getMultiMods(ruleset); var allMultiMods = getMultiMods(ruleset!);
Assert.Multiple(() => Assert.Multiple(() =>
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -33,7 +31,7 @@ namespace osu.Game.Tests.Mods
return Array.Empty<Mod>(); return Array.Empty<Mod>();
} }
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new NotImplementedException(); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException(); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new NotImplementedException();

View File

@ -62,9 +62,45 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single())); 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] [Test]

View File

@ -80,7 +80,7 @@ namespace osu.Game.Tests.Online
{ {
AddStep("download beatmap", () => beatmaps.Download(test_db_model)); 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); AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled); AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);

View File

@ -126,10 +126,10 @@ namespace osu.Game.Tests.Online
AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet)); AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet));
addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0f)); 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)); 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); addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
@ -246,7 +246,7 @@ namespace osu.Game.Tests.Online
=> new TestDownloadRequest(set); => new TestDownloadRequest(set);
} }
private class TestDownloadRequest : ArchiveDownloadRequest<IBeatmapSetInfo> internal class TestDownloadRequest : ArchiveDownloadRequest<IBeatmapSetInfo>
{ {
public new void SetProgress(float progress) => base.SetProgress(progress); public new void SetProgress(float progress) => base.SetProgress(progress);
public new void TriggerSuccess(string filename) => base.TriggerSuccess(filename); public new void TriggerSuccess(string filename) => base.TriggerSuccess(filename);

View File

@ -138,7 +138,7 @@ namespace osu.Game.Tests.Resources
BPM = bpm, BPM = bpm,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
Ruleset = rulesetInfo, Ruleset = rulesetInfo,
Metadata = metadata, Metadata = metadata.DeepClone(),
Difficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty
{ {
OverallDifficulty = diff, OverallDifficulty = diff,

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -19,8 +17,8 @@ namespace osu.Game.Tests.Rulesets.Mods
private const double start_time = 1000; private const double start_time = 1000;
private const double duration = 9000; private const double duration = 9000;
private TrackVirtual track; private TrackVirtual track = null!;
private OsuPlayfield playfield; private OsuPlayfield playfield = null!;
[SetUp] [SetUp]
public void SetUp() public void SetUp()

View File

@ -5,7 +5,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
namespace osu.Game.Tests.Visual.Editing 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("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
AddStep("scale zoom", () => TimelineArea.Timeline.Zoom = 200); 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); 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); 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] [Test]

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<string> acronyms = mods.Select(m => m.Acronym);
Assert.That(acronyms, Is.Unique);
});
}
}
}

View File

@ -8,14 +8,18 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -58,14 +62,35 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool HasCustomSteps => true; 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<FailOverlay>().First().State.Value == Visibility.Visible);
AddUntilStep("score not in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) == null));
AddStep("click save button", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().TriggerClick());
AddUntilStep("score not in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
}
[Test] [Test]
public void TestLastPlayedUpdated() public void TestLastPlayedUpdated()
{ {
DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed); DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
AddStep("set no custom ruleset", () => customRuleset = null);
AddAssert("last played is null", () => getLastPlayed() == null); AddAssert("last played is null", () => getLastPlayed() == null);
CreateTest(); CreateTest();
@ -77,8 +102,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestScoreStoredLocally() public void TestScoreStoredLocally()
{ {
AddStep("set no custom ruleset", () => customRuleset = null);
CreateTest(); CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);

View File

@ -402,16 +402,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect() public void TestPlayStartsWithCorrectBeatmapWhileAtSongSelect()
{ {
createRoom(() => new Room PlaylistItem? item = null;
createRoom(() =>
{ {
Name = { Value = "Test Room" }, item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
Playlist =
{ {
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(); pressReadyButton();
@ -419,7 +421,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () => AddStep("Enter song select", () =>
{ {
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; 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<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true); AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
@ -440,16 +442,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestPlayStartsWithCorrectRulesetWhileAtSongSelect() public void TestPlayStartsWithCorrectRulesetWhileAtSongSelect()
{ {
createRoom(() => new Room PlaylistItem? item = null;
createRoom(() =>
{ {
Name = { Value = "Test Room" }, item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
Playlist =
{ {
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(); pressReadyButton();
@ -457,7 +461,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () => AddStep("Enter song select", () =>
{ {
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; 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<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true); AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
@ -478,16 +482,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestPlayStartsWithCorrectModsWhileAtSongSelect() public void TestPlayStartsWithCorrectModsWhileAtSongSelect()
{ {
createRoom(() => new Room PlaylistItem? item = null;
createRoom(() =>
{ {
Name = { Value = "Test Room" }, item = new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
Playlist =
{ {
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(); pressReadyButton();
@ -495,7 +501,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () => AddStep("Enter song select", () =>
{ {
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerComponents.CurrentScreen).CurrentSubScreen; 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<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true); AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);

View File

@ -108,6 +108,7 @@ namespace osu.Game.Tests.Visual.Online
Version = "2018.712.0", Version = "2018.712.0",
DisplayVersion = "2018.712.0", DisplayVersion = "2018.712.0",
UpdateStream = streams[OsuGameBase.CLIENT_STREAM_NAME], UpdateStream = streams[OsuGameBase.CLIENT_STREAM_NAME],
CreatedAt = new DateTime(2018, 7, 12),
ChangelogEntries = new List<APIChangelogEntry> ChangelogEntries = new List<APIChangelogEntry>
{ {
new APIChangelogEntry new APIChangelogEntry
@ -171,6 +172,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Version = "2019.920.0", Version = "2019.920.0",
DisplayVersion = "2019.920.0", DisplayVersion = "2019.920.0",
CreatedAt = new DateTime(2019, 9, 20),
UpdateStream = new APIUpdateStream UpdateStream = new APIUpdateStream
{ {
Name = "Test", Name = "Test",

View File

@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3103765, Id = 3103765,
IsOnline = true, IsOnline = true,
Statistics = new UserStatistics { GlobalRank = 1111 }, Statistics = new UserStatistics { GlobalRank = 1111 },
Country = new Country { FlagName = "JP" }, CountryCode = CountryCode.JP,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
}, },
new APIUser new APIUser
@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 2, Id = 2,
IsOnline = false, IsOnline = false,
Statistics = new UserStatistics { GlobalRank = 2222 }, Statistics = new UserStatistics { GlobalRank = 2222 },
Country = new Country { FlagName = "AU" }, CountryCode = CountryCode.AU,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
IsSupporter = true, IsSupporter = true,
SupportLevel = 3, SupportLevel = 3,
@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Username = "Evast", Username = "Evast",
Id = 8195163, Id = 8195163,
Country = new Country { FlagName = "BY" }, CountryCode = CountryCode.BY,
CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false, IsOnline = false,
LastVisit = DateTimeOffset.Now LastVisit = DateTimeOffset.Now

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneRankingsCountryFilter() public TestSceneRankingsCountryFilter()
{ {
var countryBindable = new Bindable<Country>(); var countryBindable = new Bindable<CountryCode>();
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
@ -56,20 +56,12 @@ namespace osu.Game.Tests.Visual.Online
} }
}); });
var country = new Country const CountryCode country = CountryCode.BY;
{ const CountryCode unknown_country = CountryCode.CK;
FlagName = "BY",
FullName = "Belarus"
};
var unknownCountry = new Country
{
FlagName = "CK",
FullName = "Cook Islands"
};
AddStep("Set country", () => countryBindable.Value = country); AddStep("Set country", () => countryBindable.Value = country);
AddStep("Set null country", () => countryBindable.Value = null); AddStep("Set default country", () => countryBindable.Value = default);
AddStep("Set country with no flag", () => countryBindable.Value = unknownCountry); AddStep("Set country with no flag", () => countryBindable.Value = unknown_country);
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneRankingsHeader() public TestSceneRankingsHeader()
{ {
var countryBindable = new Bindable<Country>(); var countryBindable = new Bindable<CountryCode>();
var ruleset = new Bindable<RulesetInfo>(); var ruleset = new Bindable<RulesetInfo>();
var scope = new Bindable<RankingsScope>(); var scope = new Bindable<RankingsScope>();
@ -30,21 +30,12 @@ namespace osu.Game.Tests.Visual.Online
Ruleset = { BindTarget = ruleset } Ruleset = { BindTarget = ruleset }
}); });
var country = new Country const CountryCode country = CountryCode.BY;
{ const CountryCode unknown_country = CountryCode.CK;
FlagName = "BY",
FullName = "Belarus"
};
var unknownCountry = new Country
{
FlagName = "CK",
FullName = "Cook Islands"
};
AddStep("Set country", () => countryBindable.Value = country); AddStep("Set country", () => countryBindable.Value = country);
AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score); 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);
} }
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Online
private TestRankingsOverlay rankingsOverlay; private TestRankingsOverlay rankingsOverlay;
private readonly Bindable<Country> countryBindable = new Bindable<Country>(); private readonly Bindable<CountryCode> countryBindable = new Bindable<CountryCode>();
private readonly Bindable<RankingsScope> scope = new Bindable<RankingsScope>(); private readonly Bindable<RankingsScope> scope = new Bindable<RankingsScope>();
[SetUp] [SetUp]
@ -48,15 +48,15 @@ namespace osu.Game.Tests.Visual.Online
public void TestFlagScopeDependency() public void TestFlagScopeDependency()
{ {
AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score); AddStep("Set scope to Score", () => scope.Value = RankingsScope.Score);
AddAssert("Check country is Null", () => countryBindable.Value == null); AddAssert("Check country is default", () => countryBindable.IsDefault);
AddStep("Set country", () => countryBindable.Value = us_country); AddStep("Set country", () => countryBindable.Value = CountryCode.US);
AddAssert("Check scope is Performance", () => scope.Value == RankingsScope.Performance); AddAssert("Check scope is Performance", () => scope.Value == RankingsScope.Performance);
} }
[Test] [Test]
public void TestShowCountry() public void TestShowCountry()
{ {
AddStep("Show US", () => rankingsOverlay.ShowCountry(us_country)); AddStep("Show US", () => rankingsOverlay.ShowCountry(CountryCode.US));
} }
private void loadRankingsOverlay() 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 private class TestRankingsOverlay : RankingsOverlay
{ {
public new Bindable<Country> Country => base.Country; public new Bindable<CountryCode> Country => base.Country;
} }
} }
} }

View File

@ -57,8 +57,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
new CountryStatistics new CountryStatistics
{ {
Country = new Country { FlagName = "US", FullName = "United States" }, Code = CountryCode.US,
FlagName = "US",
ActiveUsers = 2_972_623, ActiveUsers = 2_972_623,
PlayCount = 3_086_515_743, PlayCount = 3_086_515_743,
RankedScore = 449_407_643_332_546, RankedScore = 449_407_643_332_546,
@ -66,8 +65,7 @@ namespace osu.Game.Tests.Visual.Online
}, },
new CountryStatistics new CountryStatistics
{ {
Country = new Country { FlagName = "RU", FullName = "Russian Federation" }, Code = CountryCode.RU,
FlagName = "RU",
ActiveUsers = 1_609_989, ActiveUsers = 1_609_989,
PlayCount = 1_637_052_841, PlayCount = 1_637_052_841,
RankedScore = 221_660_827_473_004, RankedScore = 221_660_827_473_004,
@ -86,7 +84,7 @@ namespace osu.Game.Tests.Visual.Online
User = new APIUser User = new APIUser
{ {
Username = "first active user", Username = "first active user",
Country = new Country { FlagName = "JP" }, CountryCode = CountryCode.JP,
Active = true, Active = true,
}, },
Accuracy = 0.9972, Accuracy = 0.9972,
@ -106,7 +104,7 @@ namespace osu.Game.Tests.Visual.Online
User = new APIUser User = new APIUser
{ {
Username = "inactive user", Username = "inactive user",
Country = new Country { FlagName = "AU" }, CountryCode = CountryCode.AU,
Active = false, Active = false,
}, },
Accuracy = 0.9831, Accuracy = 0.9831,
@ -126,7 +124,7 @@ namespace osu.Game.Tests.Visual.Online
User = new APIUser User = new APIUser
{ {
Username = "second active user", Username = "second active user",
Country = new Country { FlagName = "PL" }, CountryCode = CountryCode.PL,
Active = true, Active = true,
}, },
Accuracy = 0.9584, Accuracy = 0.9584,

View File

@ -157,11 +157,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Id = 6602580, Id = 6602580,
Username = @"waaiiru", Username = @"waaiiru",
Country = new Country CountryCode = CountryCode.ES,
{
FullName = @"Spain",
FlagName = @"ES",
},
}, },
Mods = new[] Mods = new[]
{ {
@ -184,11 +180,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Id = 4608074, Id = 4608074,
Username = @"Skycries", Username = @"Skycries",
Country = new Country CountryCode = CountryCode.BR,
{
FullName = @"Brazil",
FlagName = @"BR",
},
}, },
Mods = new[] Mods = new[]
{ {
@ -210,11 +202,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Id = 1014222, Id = 1014222,
Username = @"eLy", Username = @"eLy",
Country = new Country CountryCode = CountryCode.JP,
{
FullName = @"Japan",
FlagName = @"JP",
},
}, },
Mods = new[] Mods = new[]
{ {
@ -235,11 +223,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Id = 1541390, Id = 1541390,
Username = @"Toukai", Username = @"Toukai",
Country = new Country CountryCode = CountryCode.CA,
{
FullName = @"Canada",
FlagName = @"CA",
},
}, },
Mods = new[] Mods = new[]
{ {
@ -259,11 +243,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Id = 7151382, Id = 7151382,
Username = @"Mayuri Hana", Username = @"Mayuri Hana",
Country = new Country CountryCode = CountryCode.TH,
{
FullName = @"Thailand",
FlagName = @"TH",
},
}, },
Rank = ScoreRank.D, Rank = ScoreRank.D,
PP = 160, 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) foreach (var s in scores.Scores)
{ {
s.Statistics = new Dictionary<HitResult, int> s.Statistics = new Dictionary<HitResult, int>
{ {
{ HitResult.Great, RNG.Next(2000) }, { HitResult.Great, greatCount },
{ HitResult.Ok, RNG.Next(2000) }, { HitResult.LargeTickHit, tickCount },
{ HitResult.Meh, RNG.Next(2000) }, { HitResult.Ok, RNG.Next(100) },
{ HitResult.Miss, RNG.Next(2000) } { 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; return scores;
@ -298,11 +289,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Id = 7151382, Id = 7151382,
Username = @"Mayuri Hana", Username = @"Mayuri Hana",
Country = new Country CountryCode = CountryCode.TH,
{
FullName = @"Thailand",
FlagName = @"TH",
},
}, },
Rank = ScoreRank.D, Rank = ScoreRank.D,
PP = 160, PP = 160,

View File

@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Username = @"flyte", Username = @"flyte",
Id = 3103765, Id = 3103765,
Country = new Country { FlagName = @"JP" }, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
Status = { Value = new UserStatusOnline() } Status = { Value = new UserStatusOnline() }
}) { Width = 300 }, }) { Width = 300 },
@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Username = @"peppy", Username = @"peppy",
Id = 2, Id = 2,
Country = new Country { FlagName = @"AU" }, CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
IsSupporter = true, IsSupporter = true,
SupportLevel = 3, SupportLevel = 3,
@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Username = @"Evast", Username = @"Evast",
Id = 8195163, Id = 8195163,
Country = new Country { FlagName = @"BY" }, CountryCode = CountryCode.BY,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg", CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false, IsOnline = false,
LastVisit = DateTimeOffset.Now LastVisit = DateTimeOffset.Now

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Username = @"Somebody", Username = @"Somebody",
Id = 1, Id = 1,
Country = new Country { FullName = @"Alien" }, CountryCode = CountryCode.Unknown,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
JoinDate = DateTimeOffset.Now.AddDays(-1), JoinDate = DateTimeOffset.Now.AddDays(-1),
LastVisit = DateTimeOffset.Now, LastVisit = DateTimeOffset.Now,
@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"peppy", Username = @"peppy",
Id = 2, Id = 2,
IsSupporter = true, IsSupporter = true,
Country = new Country { FullName = @"Australia", FlagName = @"AU" }, CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg" CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg"
})); }));
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Username = @"flyte", Username = @"flyte",
Id = 3103765, Id = 3103765,
Country = new Country { FullName = @"Japan", FlagName = @"JP" }, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
})); }));
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"BanchoBot", Username = @"BanchoBot",
Id = 3, Id = 3,
IsBot = true, IsBot = true,
Country = new Country { FullName = @"Saint Helena", FlagName = @"SH" }, CountryCode = CountryCode.SH,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg" CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg"
})); }));

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
public TestSceneUserProfileScores() public TestSceneUserProfileScores()
{ {
var firstScore = new APIScore var firstScore = new SoloScoreInfo
{ {
PP = 1047.21, PP = 1047.21,
Rank = ScoreRank.SH, Rank = ScoreRank.SH,
@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Online
}, },
DifficultyName = "Extreme" DifficultyName = "Extreme"
}, },
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
Mods = new[] Mods = new[]
{ {
new APIMod { Acronym = new OsuModHidden().Acronym }, new APIMod { Acronym = new OsuModHidden().Acronym },
@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.9813 Accuracy = 0.9813
}; };
var secondScore = new APIScore var secondScore = new SoloScoreInfo
{ {
PP = 134.32, PP = 134.32,
Rank = ScoreRank.A, Rank = ScoreRank.A,
@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Online
}, },
DifficultyName = "[4K] Regret" DifficultyName = "[4K] Regret"
}, },
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
Mods = new[] Mods = new[]
{ {
new APIMod { Acronym = new OsuModHardRock().Acronym }, new APIMod { Acronym = new OsuModHardRock().Acronym },
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.998546 Accuracy = 0.998546
}; };
var thirdScore = new APIScore var thirdScore = new SoloScoreInfo
{ {
PP = 96.83, PP = 96.83,
Rank = ScoreRank.S, Rank = ScoreRank.S,
@ -79,11 +79,11 @@ namespace osu.Game.Tests.Visual.Online
}, },
DifficultyName = "Insane" DifficultyName = "Insane"
}, },
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
Accuracy = 0.9726 Accuracy = 0.9726
}; };
var noPPScore = new APIScore var noPPScore = new SoloScoreInfo
{ {
Rank = ScoreRank.B, Rank = ScoreRank.B,
Beatmap = new APIBeatmap Beatmap = new APIBeatmap
@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online
}, },
DifficultyName = "[4K] Cataclysmic Hypernova" DifficultyName = "[4K] Cataclysmic Hypernova"
}, },
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
Accuracy = 0.55879 Accuracy = 0.55879
}; };

File diff suppressed because one or more lines are too long

View File

@ -825,7 +825,8 @@ namespace osu.Game.Tests.Visual.SongSelect
checkVisibleItemCount(true, 15); checkVisibleItemCount(true, 15);
} }
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null, bool randomDifficulties = false) private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
bool randomDifficulties = false)
{ {
bool changed = false; bool changed = false;

View File

@ -140,11 +140,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 6602580, Id = 6602580,
Username = @"waaiiru", Username = @"waaiiru",
Country = new Country CountryCode = CountryCode.ES,
{
FullName = @"Spain",
FlagName = @"ES",
},
}, },
}); });
} }
@ -164,12 +160,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 6602580, Id = 6602580,
Username = @"waaiiru", Username = @"waaiiru",
Country = new Country CountryCode = CountryCode.ES,
{ }
FullName = @"Spain",
FlagName = @"ES",
},
},
}); });
} }
@ -225,11 +217,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 6602580, Id = 6602580,
Username = @"waaiiru", Username = @"waaiiru",
Country = new Country CountryCode = CountryCode.ES,
{
FullName = @"Spain",
FlagName = @"ES",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -246,11 +234,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 4608074, Id = 4608074,
Username = @"Skycries", Username = @"Skycries",
Country = new Country CountryCode = CountryCode.BR,
{
FullName = @"Brazil",
FlagName = @"BR",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -268,11 +252,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 1014222, Id = 1014222,
Username = @"eLy", Username = @"eLy",
Country = new Country CountryCode = CountryCode.JP,
{
FullName = @"Japan",
FlagName = @"JP",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -290,11 +270,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 1541390, Id = 1541390,
Username = @"Toukai", Username = @"Toukai",
Country = new Country CountryCode = CountryCode.CA,
{
FullName = @"Canada",
FlagName = @"CA",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -312,11 +288,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 2243452, Id = 2243452,
Username = @"Satoruu", Username = @"Satoruu",
Country = new Country CountryCode = CountryCode.VE,
{
FullName = @"Venezuela",
FlagName = @"VE",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -334,11 +306,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 2705430, Id = 2705430,
Username = @"Mooha", Username = @"Mooha",
Country = new Country CountryCode = CountryCode.FR,
{
FullName = @"France",
FlagName = @"FR",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -356,11 +324,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 7151382, Id = 7151382,
Username = @"Mayuri Hana", Username = @"Mayuri Hana",
Country = new Country CountryCode = CountryCode.TH,
{
FullName = @"Thailand",
FlagName = @"TH",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -378,11 +342,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 2051389, Id = 2051389,
Username = @"FunOrange", Username = @"FunOrange",
Country = new Country CountryCode = CountryCode.CA,
{
FullName = @"Canada",
FlagName = @"CA",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -400,11 +360,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 6169483, Id = 6169483,
Username = @"-Hebel-", Username = @"-Hebel-",
Country = new Country CountryCode = CountryCode.MX,
{
FullName = @"Mexico",
FlagName = @"MX",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -422,11 +378,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 6702666, Id = 6702666,
Username = @"prhtnsm", Username = @"prhtnsm",
Country = new Country CountryCode = CountryCode.DE,
{
FullName = @"Germany",
FlagName = @"DE",
},
}, },
}, },
}; };

View File

@ -0,0 +1,156 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Tests.Online;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public class TestSceneUpdateBeatmapSetButton : OsuManualInputManagerTestScene
{
private BeatmapCarousel carousel = null!;
private TestSceneOnlinePlayBeatmapAvailabilityTracker.TestBeatmapModelDownloader beatmapDownloader = null!;
private BeatmapSetInfo testBeatmapSetInfo = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
var importer = parent.Get<BeatmapManager>();
dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestSceneOnlinePlayBeatmapAvailabilityTracker.TestBeatmapModelDownloader(importer, API));
return dependencies;
}
private UpdateBeatmapSetButton? getUpdateButton() => carousel.ChildrenOfType<UpdateBeatmapSetButton>().SingleOrDefault();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create carousel", () =>
{
Child = carousel = new BeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
BeatmapSets = new List<BeatmapSetInfo>
{
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
}
};
});
AddUntilStep("wait for load", () => carousel.BeatmapSetsLoaded);
AddAssert("update button not visible", () => getUpdateButton() == null);
}
[Test]
public void TestDownloadToCompletion()
{
ArchiveDownloadRequest<IBeatmapSetInfo>? downloadRequest = null;
AddStep("update online hash", () =>
{
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
});
AddUntilStep("only one set visible", () => carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().Count() == 1);
AddUntilStep("update button visible", () => getUpdateButton() != null);
AddStep("click button", () => getUpdateButton()?.TriggerClick());
AddUntilStep("wait for download started", () =>
{
downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
return downloadRequest != null;
});
AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
AddUntilStep("progress download to completion", () =>
{
if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
{
testRequest.SetProgress(testRequest.Progress + 0.1f);
if (testRequest.Progress >= 1)
{
testRequest.TriggerSuccess();
// usually this would be done by the import process.
testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash";
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
// usually this would be done by a realm subscription.
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
return true;
}
}
return false;
});
}
[Test]
public void TestDownloadFailed()
{
ArchiveDownloadRequest<IBeatmapSetInfo>? downloadRequest = null;
AddStep("update online hash", () =>
{
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
});
AddUntilStep("only one set visible", () => carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().Count() == 1);
AddUntilStep("update button visible", () => getUpdateButton() != null);
AddStep("click button", () => getUpdateButton()?.TriggerClick());
AddUntilStep("wait for download started", () =>
{
downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo);
return downloadRequest != null;
});
AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false);
AddUntilStep("progress download to failure", () =>
{
if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
{
testRequest.SetProgress(testRequest.Progress + 0.1f);
if (testRequest.Progress >= 0.5f)
{
testRequest.TriggerFailure(new Exception());
return true;
}
}
return false;
});
AddUntilStep("wait for button enabled", () => getUpdateButton()?.Enabled.Value == true);
}
}
}

View File

@ -69,11 +69,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 6602580, Id = 6602580,
Username = @"waaiiru", Username = @"waaiiru",
Country = new Country CountryCode = CountryCode.ES,
{
FullName = @"Spain",
FlagName = @"ES",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -88,11 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 4608074, Id = 4608074,
Username = @"Skycries", Username = @"Skycries",
Country = new Country CountryCode = CountryCode.BR,
{
FullName = @"Brazil",
FlagName = @"BR",
},
}, },
}, },
new ScoreInfo new ScoreInfo
@ -107,11 +99,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
Id = 1541390, Id = 1541390,
Username = @"Toukai", Username = @"Toukai",
Country = new Country CountryCode = CountryCode.CA,
{
FullName = @"Canada",
FlagName = @"CA",
},
}, },
} }
}; };

View File

@ -4,13 +4,13 @@
#nullable disable #nullable disable
using System.Linq; using System.Linq;
using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
control.General.BindCollectionChanged((_, _) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().Underscore())) : "")}", true); control.General.BindCollectionChanged((_, _) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().ToSnakeCase())) : "")}", true);
control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);

View File

@ -0,0 +1,51 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneFPSCounter : OsuTestScene
{
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create display", () =>
{
Children = new Drawable[]
{
new Box
{
Colour = Color4.White,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new FPSCounter(),
new FPSCounter { Scale = new Vector2(2) },
new FPSCounter { Scale = new Vector2(4) },
}
},
};
});
}
[Test]
public void TestBasic()
{
}
}
}

View File

@ -0,0 +1,770 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Users;
namespace osu.Game.Tournament
{
public static class CountryExtensions
{
public static string GetAcronym(this CountryCode country)
{
switch (country)
{
case CountryCode.BD:
return "BGD";
case CountryCode.BE:
return "BEL";
case CountryCode.BF:
return "BFA";
case CountryCode.BG:
return "BGR";
case CountryCode.BA:
return "BIH";
case CountryCode.BB:
return "BRB";
case CountryCode.WF:
return "WLF";
case CountryCode.BL:
return "BLM";
case CountryCode.BM:
return "BMU";
case CountryCode.BN:
return "BRN";
case CountryCode.BO:
return "BOL";
case CountryCode.BH:
return "BHR";
case CountryCode.BI:
return "BDI";
case CountryCode.BJ:
return "BEN";
case CountryCode.BT:
return "BTN";
case CountryCode.JM:
return "JAM";
case CountryCode.BV:
return "BVT";
case CountryCode.BW:
return "BWA";
case CountryCode.WS:
return "WSM";
case CountryCode.BQ:
return "BES";
case CountryCode.BR:
return "BRA";
case CountryCode.BS:
return "BHS";
case CountryCode.JE:
return "JEY";
case CountryCode.BY:
return "BLR";
case CountryCode.BZ:
return "BLZ";
case CountryCode.RU:
return "RUS";
case CountryCode.RW:
return "RWA";
case CountryCode.RS:
return "SRB";
case CountryCode.TL:
return "TLS";
case CountryCode.RE:
return "REU";
case CountryCode.TM:
return "TKM";
case CountryCode.TJ:
return "TJK";
case CountryCode.RO:
return "ROU";
case CountryCode.TK:
return "TKL";
case CountryCode.GW:
return "GNB";
case CountryCode.GU:
return "GUM";
case CountryCode.GT:
return "GTM";
case CountryCode.GS:
return "SGS";
case CountryCode.GR:
return "GRC";
case CountryCode.GQ:
return "GNQ";
case CountryCode.GP:
return "GLP";
case CountryCode.JP:
return "JPN";
case CountryCode.GY:
return "GUY";
case CountryCode.GG:
return "GGY";
case CountryCode.GF:
return "GUF";
case CountryCode.GE:
return "GEO";
case CountryCode.GD:
return "GRD";
case CountryCode.GB:
return "GBR";
case CountryCode.GA:
return "GAB";
case CountryCode.SV:
return "SLV";
case CountryCode.GN:
return "GIN";
case CountryCode.GM:
return "GMB";
case CountryCode.GL:
return "GRL";
case CountryCode.GI:
return "GIB";
case CountryCode.GH:
return "GHA";
case CountryCode.OM:
return "OMN";
case CountryCode.TN:
return "TUN";
case CountryCode.JO:
return "JOR";
case CountryCode.HR:
return "HRV";
case CountryCode.HT:
return "HTI";
case CountryCode.HU:
return "HUN";
case CountryCode.HK:
return "HKG";
case CountryCode.HN:
return "HND";
case CountryCode.HM:
return "HMD";
case CountryCode.VE:
return "VEN";
case CountryCode.PR:
return "PRI";
case CountryCode.PS:
return "PSE";
case CountryCode.PW:
return "PLW";
case CountryCode.PT:
return "PRT";
case CountryCode.SJ:
return "SJM";
case CountryCode.PY:
return "PRY";
case CountryCode.IQ:
return "IRQ";
case CountryCode.PA:
return "PAN";
case CountryCode.PF:
return "PYF";
case CountryCode.PG:
return "PNG";
case CountryCode.PE:
return "PER";
case CountryCode.PK:
return "PAK";
case CountryCode.PH:
return "PHL";
case CountryCode.PN:
return "PCN";
case CountryCode.PL:
return "POL";
case CountryCode.PM:
return "SPM";
case CountryCode.ZM:
return "ZMB";
case CountryCode.EH:
return "ESH";
case CountryCode.EE:
return "EST";
case CountryCode.EG:
return "EGY";
case CountryCode.ZA:
return "ZAF";
case CountryCode.EC:
return "ECU";
case CountryCode.IT:
return "ITA";
case CountryCode.VN:
return "VNM";
case CountryCode.SB:
return "SLB";
case CountryCode.ET:
return "ETH";
case CountryCode.SO:
return "SOM";
case CountryCode.ZW:
return "ZWE";
case CountryCode.SA:
return "SAU";
case CountryCode.ES:
return "ESP";
case CountryCode.ER:
return "ERI";
case CountryCode.ME:
return "MNE";
case CountryCode.MD:
return "MDA";
case CountryCode.MG:
return "MDG";
case CountryCode.MF:
return "MAF";
case CountryCode.MA:
return "MAR";
case CountryCode.MC:
return "MCO";
case CountryCode.UZ:
return "UZB";
case CountryCode.MM:
return "MMR";
case CountryCode.ML:
return "MLI";
case CountryCode.MO:
return "MAC";
case CountryCode.MN:
return "MNG";
case CountryCode.MH:
return "MHL";
case CountryCode.MK:
return "MKD";
case CountryCode.MU:
return "MUS";
case CountryCode.MT:
return "MLT";
case CountryCode.MW:
return "MWI";
case CountryCode.MV:
return "MDV";
case CountryCode.MQ:
return "MTQ";
case CountryCode.MP:
return "MNP";
case CountryCode.MS:
return "MSR";
case CountryCode.MR:
return "MRT";
case CountryCode.IM:
return "IMN";
case CountryCode.UG:
return "UGA";
case CountryCode.TZ:
return "TZA";
case CountryCode.MY:
return "MYS";
case CountryCode.MX:
return "MEX";
case CountryCode.IL:
return "ISR";
case CountryCode.FR:
return "FRA";
case CountryCode.IO:
return "IOT";
case CountryCode.SH:
return "SHN";
case CountryCode.FI:
return "FIN";
case CountryCode.FJ:
return "FJI";
case CountryCode.FK:
return "FLK";
case CountryCode.FM:
return "FSM";
case CountryCode.FO:
return "FRO";
case CountryCode.NI:
return "NIC";
case CountryCode.NL:
return "NLD";
case CountryCode.NO:
return "NOR";
case CountryCode.NA:
return "NAM";
case CountryCode.VU:
return "VUT";
case CountryCode.NC:
return "NCL";
case CountryCode.NE:
return "NER";
case CountryCode.NF:
return "NFK";
case CountryCode.NG:
return "NGA";
case CountryCode.NZ:
return "NZL";
case CountryCode.NP:
return "NPL";
case CountryCode.NR:
return "NRU";
case CountryCode.NU:
return "NIU";
case CountryCode.CK:
return "COK";
case CountryCode.XK:
return "XKX";
case CountryCode.CI:
return "CIV";
case CountryCode.CH:
return "CHE";
case CountryCode.CO:
return "COL";
case CountryCode.CN:
return "CHN";
case CountryCode.CM:
return "CMR";
case CountryCode.CL:
return "CHL";
case CountryCode.CC:
return "CCK";
case CountryCode.CA:
return "CAN";
case CountryCode.CG:
return "COG";
case CountryCode.CF:
return "CAF";
case CountryCode.CD:
return "COD";
case CountryCode.CZ:
return "CZE";
case CountryCode.CY:
return "CYP";
case CountryCode.CX:
return "CXR";
case CountryCode.CR:
return "CRI";
case CountryCode.CW:
return "CUW";
case CountryCode.CV:
return "CPV";
case CountryCode.CU:
return "CUB";
case CountryCode.SZ:
return "SWZ";
case CountryCode.SY:
return "SYR";
case CountryCode.SX:
return "SXM";
case CountryCode.KG:
return "KGZ";
case CountryCode.KE:
return "KEN";
case CountryCode.SS:
return "SSD";
case CountryCode.SR:
return "SUR";
case CountryCode.KI:
return "KIR";
case CountryCode.KH:
return "KHM";
case CountryCode.KN:
return "KNA";
case CountryCode.KM:
return "COM";
case CountryCode.ST:
return "STP";
case CountryCode.SK:
return "SVK";
case CountryCode.KR:
return "KOR";
case CountryCode.SI:
return "SVN";
case CountryCode.KP:
return "PRK";
case CountryCode.KW:
return "KWT";
case CountryCode.SN:
return "SEN";
case CountryCode.SM:
return "SMR";
case CountryCode.SL:
return "SLE";
case CountryCode.SC:
return "SYC";
case CountryCode.KZ:
return "KAZ";
case CountryCode.KY:
return "CYM";
case CountryCode.SG:
return "SGP";
case CountryCode.SE:
return "SWE";
case CountryCode.SD:
return "SDN";
case CountryCode.DO:
return "DOM";
case CountryCode.DM:
return "DMA";
case CountryCode.DJ:
return "DJI";
case CountryCode.DK:
return "DNK";
case CountryCode.VG:
return "VGB";
case CountryCode.DE:
return "DEU";
case CountryCode.YE:
return "YEM";
case CountryCode.DZ:
return "DZA";
case CountryCode.US:
return "USA";
case CountryCode.UY:
return "URY";
case CountryCode.YT:
return "MYT";
case CountryCode.UM:
return "UMI";
case CountryCode.LB:
return "LBN";
case CountryCode.LC:
return "LCA";
case CountryCode.LA:
return "LAO";
case CountryCode.TV:
return "TUV";
case CountryCode.TW:
return "TWN";
case CountryCode.TT:
return "TTO";
case CountryCode.TR:
return "TUR";
case CountryCode.LK:
return "LKA";
case CountryCode.LI:
return "LIE";
case CountryCode.LV:
return "LVA";
case CountryCode.TO:
return "TON";
case CountryCode.LT:
return "LTU";
case CountryCode.LU:
return "LUX";
case CountryCode.LR:
return "LBR";
case CountryCode.LS:
return "LSO";
case CountryCode.TH:
return "THA";
case CountryCode.TF:
return "ATF";
case CountryCode.TG:
return "TGO";
case CountryCode.TD:
return "TCD";
case CountryCode.TC:
return "TCA";
case CountryCode.LY:
return "LBY";
case CountryCode.VA:
return "VAT";
case CountryCode.VC:
return "VCT";
case CountryCode.AE:
return "ARE";
case CountryCode.AD:
return "AND";
case CountryCode.AG:
return "ATG";
case CountryCode.AF:
return "AFG";
case CountryCode.AI:
return "AIA";
case CountryCode.VI:
return "VIR";
case CountryCode.IS:
return "ISL";
case CountryCode.IR:
return "IRN";
case CountryCode.AM:
return "ARM";
case CountryCode.AL:
return "ALB";
case CountryCode.AO:
return "AGO";
case CountryCode.AQ:
return "ATA";
case CountryCode.AS:
return "ASM";
case CountryCode.AR:
return "ARG";
case CountryCode.AU:
return "AUS";
case CountryCode.AT:
return "AUT";
case CountryCode.AW:
return "ABW";
case CountryCode.IN:
return "IND";
case CountryCode.AX:
return "ALA";
case CountryCode.AZ:
return "AZE";
case CountryCode.IE:
return "IRL";
case CountryCode.ID:
return "IDN";
case CountryCode.UA:
return "UKR";
case CountryCode.QA:
return "QAT";
case CountryCode.MZ:
return "MOZ";
default:
throw new ArgumentOutOfRangeException(nameof(country));
}
}
}
}

View File

@ -22,7 +22,8 @@ namespace osu.Game.Tournament.Models
/// <summary> /// <summary>
/// The player's country. /// The player's country.
/// </summary> /// </summary>
public Country? Country { get; set; } [JsonProperty("country_code")]
public CountryCode CountryCode { get; set; }
/// <summary> /// <summary>
/// The player's global rank, or null if not available. /// The player's global rank, or null if not available.
@ -40,7 +41,7 @@ namespace osu.Game.Tournament.Models
{ {
Id = OnlineID, Id = OnlineID,
Username = Username, Username = Username,
Country = Country, CountryCode = CountryCode,
CoverUrl = CoverUrl, CoverUrl = CoverUrl,
}; };

File diff suppressed because it is too large Load Diff

View File

@ -3,13 +3,13 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -25,9 +25,6 @@ namespace osu.Game.Tournament.Screens.Editors
{ {
public class TeamEditorScreen : TournamentEditorScreen<TeamEditorScreen.TeamRow, TournamentTeam> public class TeamEditorScreen : TournamentEditorScreen<TeamEditorScreen.TeamRow, TournamentTeam>
{ {
[Resolved]
private TournamentGameBase game { get; set; }
protected override BindableList<TournamentTeam> Storage => LadderInfo.Teams; protected override BindableList<TournamentTeam> Storage => LadderInfo.Teams;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -45,11 +42,17 @@ namespace osu.Game.Tournament.Screens.Editors
private void addAllCountries() private void addAllCountries()
{ {
List<TournamentTeam> countries; var countries = new List<TournamentTeam>();
using (Stream stream = game.Resources.GetStream("Resources/countries.json")) foreach (var country in Enum.GetValues(typeof(CountryCode)).Cast<CountryCode>().Skip(1))
using (var sr = new StreamReader(stream)) {
countries = JsonConvert.DeserializeObject<List<TournamentTeam>>(sr.ReadToEnd()); countries.Add(new TournamentTeam
{
FlagName = { Value = country.ToString() },
FullName = { Value = country.GetDescription() },
Acronym = { Value = country.GetAcronym() },
});
}
Debug.Assert(countries != null); Debug.Assert(countries != null);

View File

@ -21,6 +21,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Tournament.IO; using osu.Game.Tournament.IO;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Users;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tournament namespace osu.Game.Tournament
@ -186,7 +187,9 @@ namespace osu.Game.Tournament
{ {
var playersRequiringPopulation = ladder.Teams var playersRequiringPopulation = ladder.Teams
.SelectMany(t => t.Players) .SelectMany(t => t.Players)
.Where(p => string.IsNullOrEmpty(p.Username) || p.Rank == null).ToList(); .Where(p => string.IsNullOrEmpty(p.Username)
|| p.CountryCode == CountryCode.Unknown
|| p.Rank == null).ToList();
if (playersRequiringPopulation.Count == 0) if (playersRequiringPopulation.Count == 0)
return false; return false;
@ -288,7 +291,7 @@ namespace osu.Game.Tournament
user.Username = res.Username; user.Username = res.Username;
user.CoverUrl = res.CoverUrl; user.CoverUrl = res.CoverUrl;
user.Country = res.Country; user.CountryCode = res.CountryCode;
user.Rank = res.Statistics?.GlobalRank; user.Rank = res.Statistics?.GlobalRank;
success?.Invoke(); success?.Invoke();

View File

@ -14,15 +14,15 @@ namespace osu.Game.Audio
[Serializable] [Serializable]
public class HitSampleInfo : ISampleInfo, IEquatable<HitSampleInfo> public class HitSampleInfo : ISampleInfo, IEquatable<HitSampleInfo>
{ {
public const string HIT_NORMAL = @"hitnormal";
public const string HIT_WHISTLE = @"hitwhistle"; public const string HIT_WHISTLE = @"hitwhistle";
public const string HIT_FINISH = @"hitfinish"; public const string HIT_FINISH = @"hitfinish";
public const string HIT_NORMAL = @"hitnormal";
public const string HIT_CLAP = @"hitclap"; public const string HIT_CLAP = @"hitclap";
/// <summary> /// <summary>
/// All valid sample addition constants. /// All valid sample addition constants.
/// </summary> /// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH }; public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
/// <summary> /// <summary>
/// The name of the sample to load. /// The name of the sample to load.

View File

@ -92,6 +92,16 @@ namespace osu.Game.Beatmaps
[Indexed] [Indexed]
public string MD5Hash { get; set; } = string.Empty; public string MD5Hash { get; set; } = string.Empty;
public string OnlineMD5Hash { get; set; } = string.Empty;
public DateTimeOffset? LastOnlineUpdate { get; set; }
/// <summary>
/// Whether this beatmap matches the online version, based on fetched online metadata.
/// Will return <c>true</c> if no online metadata is available.
/// </summary>
public bool MatchesOnlineVersion => LastOnlineUpdate == null || MD5Hash == OnlineMD5Hash;
[JsonIgnore] [JsonIgnore]
public bool Hidden { get; set; } public bool Hidden { get; set; }
@ -169,8 +179,8 @@ namespace osu.Game.Beatmaps
Debug.Assert(x.BeatmapSet != null); Debug.Assert(x.BeatmapSet != null);
Debug.Assert(y.BeatmapSet != null); Debug.Assert(y.BeatmapSet != null);
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.BeatmapSet.Metadata))?.File.Hash; string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.BeatmapSet.Metadata))?.File.Hash; string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash;
return fileHashX == fileHashY; return fileHashX == fileHashY;
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
@ -14,7 +12,7 @@ namespace osu.Game.Beatmaps
protected override ArchiveDownloadRequest<IBeatmapSetInfo> CreateDownloadRequest(IBeatmapSetInfo set, bool minimiseDownloadSize) => protected override ArchiveDownloadRequest<IBeatmapSetInfo> CreateDownloadRequest(IBeatmapSetInfo set, bool minimiseDownloadSize) =>
new DownloadBeatmapSetRequest(set, minimiseDownloadSize); new DownloadBeatmapSetRequest(set, minimiseDownloadSize);
public override ArchiveDownloadRequest<IBeatmapSetInfo> GetExistingDownload(IBeatmapSetInfo model) public override ArchiveDownloadRequest<IBeatmapSetInfo>? GetExistingDownload(IBeatmapSetInfo model)
=> CurrentDownloads.Find(r => r.Model.OnlineID == model.OnlineID); => CurrentDownloads.Find(r => r.Model.OnlineID == model.OnlineID);
public BeatmapModelDownloader(IModelImporter<BeatmapSetInfo> beatmapImporter, IAPIProvider api) public BeatmapModelDownloader(IModelImporter<BeatmapSetInfo> beatmapImporter, IAPIProvider api)

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Database;
using osu.Game.Online.Metadata;
namespace osu.Game.Beatmaps
{
/// <summary>
/// Ingests any changes that happen externally to the client, reprocessing as required.
/// </summary>
public class BeatmapOnlineChangeIngest : Component
{
private readonly BeatmapUpdater beatmapUpdater;
private readonly RealmAccess realm;
private readonly MetadataClient metadataClient;
public BeatmapOnlineChangeIngest(BeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient)
{
this.beatmapUpdater = beatmapUpdater;
this.realm = realm;
this.metadataClient = metadataClient;
metadataClient.ChangedBeatmapSetsArrived += changesDetected;
}
private void changesDetected(int[] beatmapSetIds)
{
// May want to batch incoming updates further if the background realm operations ever becomes a concern.
realm.Run(r =>
{
foreach (int id in beatmapSetIds)
{
var matchingSet = r.All<BeatmapSetInfo>().FirstOrDefault(s => s.OnlineID == id);
if (matchingSet != null)
beatmapUpdater.Queue(matchingSet.ToLive(realm));
}
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
metadataClient.ChangedBeatmapSetsArrived -= changesDetected;
}
}
}

View File

@ -102,6 +102,12 @@ namespace osu.Game.Beatmaps
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None; beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID; beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
beatmapInfo.OnlineMD5Hash = res.MD5Hash;
beatmapInfo.LastOnlineUpdate = res.LastUpdated;
beatmapInfo.OnlineID = res.OnlineID; beatmapInfo.OnlineID = res.OnlineID;
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID; beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
@ -190,7 +196,8 @@ namespace osu.Game.Beatmaps
using (var cmd = db.CreateCommand()) using (var cmd = db.CreateCommand())
{ {
cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path"; cmd.CommandText =
"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash)); cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash));
cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID)); cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID));
@ -208,10 +215,13 @@ namespace osu.Game.Beatmaps
beatmapInfo.BeatmapSet.Status = status; beatmapInfo.BeatmapSet.Status = status;
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0); beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
// TODO: DateSubmitted and DateRanked are not provided by local cache.
beatmapInfo.OnlineID = reader.GetInt32(1); beatmapInfo.OnlineID = reader.GetInt32(1);
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3); beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
beatmapInfo.OnlineMD5Hash = reader.GetString(4);
beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5);
logForModel(set, $"Cached local retrieval for {beatmapInfo}."); logForModel(set, $"Cached local retrieval for {beatmapInfo}.");
return true; return true;
} }

View File

@ -26,6 +26,16 @@ namespace osu.Game.Beatmaps
public DateTimeOffset DateAdded { get; set; } public DateTimeOffset DateAdded { get; set; }
/// <summary>
/// The date this beatmap set was first submitted.
/// </summary>
public DateTimeOffset? DateSubmitted { get; set; }
/// <summary>
/// The date this beatmap set was ranked.
/// </summary>
public DateTimeOffset? DateRanked { get; set; }
[JsonIgnore] [JsonIgnore]
public IBeatmapMetadataInfo Metadata => Beatmaps.FirstOrDefault()?.Metadata ?? new BeatmapMetadata(); public IBeatmapMetadataInfo Metadata => Beatmaps.FirstOrDefault()?.Metadata ?? new BeatmapMetadata();
@ -93,5 +103,7 @@ namespace osu.Game.Beatmaps
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps; IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => Beatmaps;
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files; IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
public bool AllBeatmapsUpToDate => Beatmaps.All(b => b.MatchesOnlineVersion);
} }
} }

View File

@ -3,10 +3,10 @@
#nullable disable #nullable disable
using Humanizer;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Extensions;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(TextureStore textures) private void load(TextureStore textures)
{ {
Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().Kebaberize()}"); Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().ToKebabCase()}");
} }
} }

View File

@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -30,21 +31,12 @@ namespace osu.Game.Beatmaps
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
} }
/// <summary>
/// Queue a beatmap for background processing.
/// </summary>
public void Queue(int beatmapSetId)
{
// TODO: implement
}
/// <summary> /// <summary>
/// Queue a beatmap for background processing. /// Queue a beatmap for background processing.
/// </summary> /// </summary>
public void Queue(Live<BeatmapSetInfo> beatmap) public void Queue(Live<BeatmapSetInfo> beatmap)
{ {
// For now, just fire off a task. Logger.Log($"Queueing change for local beatmap {beatmap}");
// TODO: Add actual queueing probably.
Task.Factory.StartNew(() => beatmap.PerformRead(Process)); Task.Factory.StartNew(() => beatmap.PerformRead(Process));
} }
@ -56,6 +48,8 @@ namespace osu.Game.Beatmaps
// Before we use below, we want to invalidate. // Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet); workingBeatmapCache.Invalidate(beatmapSet);
// TODO: this call currently uses the local `online.db` lookup.
// We probably don't want this to happen after initial import (as the data may be stale).
onlineLookupQueue.Update(beatmapSet); onlineLookupQueue.Update(beatmapSet);
foreach (var beatmap in beatmapSet.Beatmaps) foreach (var beatmap in beatmapSet.Beatmaps)

View File

@ -168,7 +168,7 @@ namespace osu.Game.Beatmaps
if (texture == null) if (texture == null)
{ {
Logger.Log($"Beatmap background failed to load (file {Metadata.BackgroundFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error); Logger.Log($"Beatmap background failed to load (file {Metadata.BackgroundFile} not found on disk at expected location {fileStorePath}).");
return null; return null;
} }

View File

@ -224,6 +224,12 @@ namespace osu.Game.Configuration
return new TrackedSettings return new TrackedSettings
{ {
new TrackedSetting<bool>(OsuSetting.ShowFpsDisplay, state => new SettingDescription(
rawValue: state,
name: GlobalActionKeyBindingStrings.ToggleFPSCounter,
value: state ? CommonStrings.Enabled.ToLower() : CommonStrings.Disabled.ToLower(),
shortcut: LookupKeyBindings(GlobalAction.ToggleFPSDisplay))
),
new TrackedSetting<bool>(OsuSetting.MouseDisableButtons, disabledState => new SettingDescription( new TrackedSetting<bool>(OsuSetting.MouseDisableButtons, disabledState => new SettingDescription(
rawValue: !disabledState, rawValue: !disabledState,
name: GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons, name: GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons,

View File

@ -443,7 +443,6 @@ namespace osu.Game.Database
TotalScore = score.TotalScore, TotalScore = score.TotalScore,
MaxCombo = score.MaxCombo, MaxCombo = score.MaxCombo,
Accuracy = score.Accuracy, Accuracy = score.Accuracy,
HasReplay = ((IScoreInfo)score).HasReplay,
Date = score.Date, Date = score.Date,
PP = score.PP, PP = score.PP,
Rank = score.Rank, Rank = score.Rank,

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -19,18 +17,18 @@ namespace osu.Game.Database
where TModel : class, IHasGuidPrimaryKey, ISoftDelete, IEquatable<TModel>, T where TModel : class, IHasGuidPrimaryKey, ISoftDelete, IEquatable<TModel>, T
where T : class where T : class
{ {
public Action<Notification> PostNotification { protected get; set; } public Action<Notification>? PostNotification { protected get; set; }
public event Action<ArchiveDownloadRequest<T>> DownloadBegan; public event Action<ArchiveDownloadRequest<T>>? DownloadBegan;
public event Action<ArchiveDownloadRequest<T>> DownloadFailed; public event Action<ArchiveDownloadRequest<T>>? DownloadFailed;
private readonly IModelImporter<TModel> importer; private readonly IModelImporter<TModel> importer;
private readonly IAPIProvider api; private readonly IAPIProvider? api;
protected readonly List<ArchiveDownloadRequest<T>> CurrentDownloads = new List<ArchiveDownloadRequest<T>>(); protected readonly List<ArchiveDownloadRequest<T>> CurrentDownloads = new List<ArchiveDownloadRequest<T>>();
protected ModelDownloader(IModelImporter<TModel> importer, IAPIProvider api) protected ModelDownloader(IModelImporter<TModel> importer, IAPIProvider? api)
{ {
this.importer = importer; this.importer = importer;
this.api = api; this.api = api;
@ -87,7 +85,7 @@ namespace osu.Game.Database
CurrentDownloads.Add(request); CurrentDownloads.Add(request);
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);
api.PerformAsync(request); api?.PerformAsync(request);
DownloadBegan?.Invoke(request); DownloadBegan?.Invoke(request);
return true; return true;
@ -105,7 +103,7 @@ namespace osu.Game.Database
} }
} }
public abstract ArchiveDownloadRequest<T> GetExistingDownload(T model); public abstract ArchiveDownloadRequest<T>? GetExistingDownload(T model);
private bool canDownload(T model) => GetExistingDownload(model) == null && api != null; private bool canDownload(T model) => GetExistingDownload(model) == null && api != null;

View File

@ -59,8 +59,12 @@ namespace osu.Game.Database
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields). /// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo. /// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
/// 15 2022-07-13 Added LastPlayed to BeatmapInfo. /// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
/// 16 2022-07-15 Removed HasReplay from ScoreInfo.
/// 17 2022-07-16 Added CountryCode to RealmUser.
/// 18 2022-07-19 Added OnlineMD5Hash and LastOnlineUpdate to BeatmapInfo.
/// 19 2022-07-19 Added DateSubmitted and DateRanked to BeatmapSetInfo.
/// </summary> /// </summary>
private const int schema_version = 15; private const int schema_version = 19;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -67,7 +66,7 @@ namespace osu.Game.Extensions
foreach (var (_, property) in component.GetSettingsSourceProperties()) foreach (var (_, property) in component.GetSettingsSourceProperties())
{ {
if (!info.Settings.TryGetValue(property.Name.Underscore(), out object settingValue)) if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
continue; continue;
skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue); skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue);

View File

@ -0,0 +1,94 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
// Based on code from the Humanizer library (https://github.com/Humanizr/Humanizer/blob/606e958cb83afc9be5b36716ac40d4daa9fa73a7/src/Humanizer/InflectorExtensions.cs)
//
// Humanizer is licenced under the MIT License (MIT)
//
// Copyright (c) .NET Foundation and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System.Text.RegularExpressions;
namespace osu.Game.Extensions
{
/// <summary>
/// Class with extension methods used to turn human-readable strings to casing conventions frequently used in code.
/// Often used for communicating with other systems (web API, spectator server).
/// All of the operations in this class are intentionally culture-invariant.
/// </summary>
public static class StringDehumanizeExtensions
{
/// <summary>
/// Converts the string to "Pascal case" (also known as "upper camel case").
/// </summary>
/// <example>
/// <code>
/// "this is a test string".ToPascalCase() == "ThisIsATestString"
/// </code>
/// </example>
public static string ToPascalCase(this string input)
{
return Regex.Replace(input, "(?:^|_|-| +)(.)", match => match.Groups[1].Value.ToUpperInvariant());
}
/// <summary>
/// Converts the string to (lower) "camel case".
/// </summary>
/// <example>
/// <code>
/// "this is a test string".ToCamelCase() == "thisIsATestString"
/// </code>
/// </example>
public static string ToCamelCase(this string input)
{
string word = input.ToPascalCase();
return word.Length > 0 ? word.Substring(0, 1).ToLowerInvariant() + word.Substring(1) : word;
}
/// <summary>
/// Converts the string to "snake case".
/// </summary>
/// <example>
/// <code>
/// "this is a test string".ToSnakeCase() == "this_is_a_test_string"
/// </code>
/// </example>
public static string ToSnakeCase(this string input)
{
return Regex.Replace(
Regex.Replace(
Regex.Replace(input, @"([\p{Lu}]+)([\p{Lu}][\p{Ll}])", "$1_$2"), @"([\p{Ll}\d])([\p{Lu}])", "$1_$2"), @"[-\s]", "_").ToLowerInvariant();
}
/// <summary>
/// Converts the string to "kebab case".
/// </summary>
/// <example>
/// <code>
/// "this is a test string".ToKebabCase() == "this-is-a-test-string"
/// </code>
/// </example>
public static string ToKebabCase(this string input)
{
return ToSnakeCase(input).Replace('_', '-');
}
}
}

View File

@ -28,7 +28,8 @@ namespace osu.Game.Graphics.Containers
public class BeatSyncedContainer : Container public class BeatSyncedContainer : Container
{ {
private int lastBeat; private int lastBeat;
private TimingControlPoint lastTimingPoint; protected TimingControlPoint LastTimingPoint { get; private set; }
protected EffectControlPoint LastEffectPoint { get; private set; }
/// <summary> /// <summary>
/// The amount of time before a beat we should fire <see cref="OnNewBeat(int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes)"/>. /// The amount of time before a beat we should fire <see cref="OnNewBeat(int, TimingControlPoint, EffectControlPoint, ChannelAmplitudes)"/>.
@ -127,7 +128,7 @@ namespace osu.Game.Graphics.Containers
TimeSinceLastBeat = beatLength - TimeUntilNextBeat; TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
if (ReferenceEquals(timingPoint, lastTimingPoint) && beatIndex == lastBeat) if (ReferenceEquals(timingPoint, LastTimingPoint) && beatIndex == lastBeat)
return; return;
// as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat. // as this event is sometimes used for sound triggers where `BeginDelayedSequence` has no effect, avoid firing it if too far away from the beat.
@ -139,7 +140,8 @@ namespace osu.Game.Graphics.Containers
} }
lastBeat = beatIndex; lastBeat = beatIndex;
lastTimingPoint = timingPoint; LastTimingPoint = timingPoint;
LastEffectPoint = effectPoint;
} }
} }
} }

View File

@ -3,21 +3,21 @@
#nullable disable #nullable disable
using osuTK; using System;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using System;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Graphics.Cursor namespace osu.Game.Graphics.Cursor
{ {
@ -35,6 +35,7 @@ namespace osu.Game.Graphics.Cursor
private Vector2 positionMouseDown; private Vector2 positionMouseDown;
private Sample tapSample; private Sample tapSample;
private Vector2 lastMovePosition;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio) private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio)
@ -47,16 +48,25 @@ namespace osu.Game.Graphics.Cursor
tapSample = audio.Samples.Get(@"UI/cursor-tap"); tapSample = audio.Samples.Get(@"UI/cursor-tap");
} }
protected override void Update()
{
base.Update();
if (dragRotationState != DragRotationState.NotDragging
&& Vector2.Distance(positionMouseDown, lastMovePosition) > 60)
{
// make the rotation centre point floating.
positionMouseDown = Interpolation.ValueAt(0.04f, positionMouseDown, lastMovePosition, 0, Clock.ElapsedFrameTime);
}
}
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
if (dragRotationState != DragRotationState.NotDragging) if (dragRotationState != DragRotationState.NotDragging)
{ {
// make the rotation centre point floating. lastMovePosition = e.MousePosition;
if (Vector2.Distance(positionMouseDown, e.MousePosition) > 60)
positionMouseDown = Interpolation.ValueAt(0.005f, positionMouseDown, e.MousePosition, 0, Clock.ElapsedFrameTime);
var position = e.MousePosition; float distance = Vector2Extensions.Distance(lastMovePosition, positionMouseDown);
float distance = Vector2Extensions.Distance(position, positionMouseDown);
// don't start rotating until we're moved a minimum distance away from the mouse down location, // don't start rotating until we're moved a minimum distance away from the mouse down location,
// else it can have an annoying effect. // else it can have an annoying effect.

View File

@ -0,0 +1,282 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
public class FPSCounter : VisibilityContainer, IHasCustomTooltip
{
private OsuSpriteText counterUpdateFrameTime = null!;
private OsuSpriteText counterDrawFPS = null!;
private Container mainContent = null!;
private Container background = null!;
private Container counters = null!;
private const double min_time_between_updates = 10;
private const double spike_time_ms = 20;
private const float idle_background_alpha = 0.4f;
private readonly BindableBool showFpsDisplay = new BindableBool(true);
private double displayedFpsCount;
private double displayedFrameTime;
private bool isDisplayed;
private ScheduledDelegate? fadeOutDelegate;
private double aimDrawFPS;
private double aimUpdateFPS;
private double lastUpdate;
private ThrottledFrameClock drawClock = null!;
private ThrottledFrameClock updateClock = null!;
private ThrottledFrameClock inputClock = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public FPSCounter()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, GameHost gameHost)
{
InternalChildren = new Drawable[]
{
mainContent = new Container
{
Alpha = 0,
Height = 26,
Children = new Drawable[]
{
background = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 5,
CornerExponent = 5f,
Masking = true,
Alpha = idle_background_alpha,
Children = new Drawable[]
{
new Box
{
Colour = colours.Gray0,
RelativeSizeAxes = Axes.Both,
},
}
},
counters = new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
counterUpdateFrameTime = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(1),
Font = OsuFont.Default.With(fixedWidth: true, size: 16, weight: FontWeight.SemiBold),
Spacing = new Vector2(-1),
Y = -2,
},
counterDrawFPS = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(2),
Font = OsuFont.Default.With(fixedWidth: true, size: 13, weight: FontWeight.SemiBold),
Spacing = new Vector2(-2),
Y = 10,
}
}
},
}
},
};
config.BindWith(OsuSetting.ShowFpsDisplay, showFpsDisplay);
drawClock = gameHost.DrawThread.Clock;
updateClock = gameHost.UpdateThread.Clock;
inputClock = gameHost.InputThread.Clock;
}
protected override void LoadComplete()
{
base.LoadComplete();
displayTemporarily();
showFpsDisplay.BindValueChanged(showFps =>
{
State.Value = showFps.NewValue ? Visibility.Visible : Visibility.Hidden;
if (showFps.NewValue)
displayTemporarily();
}, true);
State.BindValueChanged(state => showFpsDisplay.Value = state.NewValue == Visibility.Visible);
}
protected override void PopIn() => this.FadeIn(100);
protected override void PopOut() => this.FadeOut(100);
protected override bool OnHover(HoverEvent e)
{
background.FadeTo(1, 200);
displayTemporarily();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
background.FadeTo(idle_background_alpha, 200);
displayTemporarily();
base.OnHoverLost(e);
}
private void displayTemporarily()
{
if (!isDisplayed)
{
mainContent.FadeTo(1, 300, Easing.OutQuint);
isDisplayed = true;
}
fadeOutDelegate?.Cancel();
fadeOutDelegate = null;
if (!IsHovered)
{
fadeOutDelegate = Scheduler.AddDelayed(() =>
{
mainContent.FadeTo(0, 300, Easing.OutQuint);
isDisplayed = false;
}, 2000);
}
}
protected override void Update()
{
base.Update();
mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth);
// Handle the case where the window has become inactive or the user changed the
// frame limiter (we want to show the FPS as it's changing, even if it isn't an outlier).
bool aimRatesChanged = updateAimFPS();
bool hasUpdateSpike = displayedFrameTime < spike_time_ms && updateClock.ElapsedFrameTime > spike_time_ms;
// use elapsed frame time rather then FramesPerSecond to better catch stutter frames.
bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms;
// note that we use an elapsed time here of 1 intentionally.
// this weights all updates equally. if we passed in the elapsed time, longer frames would be weighted incorrectly lower.
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : 100, 1);
if (hasDrawSpike)
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show.
displayedFpsCount = 1000 / drawClock.ElapsedFrameTime;
else
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, 100, Time.Elapsed);
if (Time.Current - lastUpdate > min_time_between_updates)
{
updateFpsDisplay();
updateFrameTimeDisplay();
lastUpdate = Time.Current;
}
bool hasSignificantChanges = aimRatesChanged
|| hasDrawSpike
|| hasUpdateSpike
|| displayedFpsCount < aimDrawFPS * 0.8
|| 1000 / displayedFrameTime < aimUpdateFPS * 0.8;
if (hasSignificantChanges)
displayTemporarily();
}
private void updateFpsDisplay()
{
counterDrawFPS.Colour = getColour(displayedFpsCount / aimDrawFPS);
counterDrawFPS.Text = $"{displayedFpsCount:#,0}fps";
}
private void updateFrameTimeDisplay()
{
counterUpdateFrameTime.Text = displayedFrameTime < 5
? $"{displayedFrameTime:N1}ms"
: $"{displayedFrameTime:N0}ms";
counterUpdateFrameTime.Colour = getColour((1000 / displayedFrameTime) / aimUpdateFPS);
}
private bool updateAimFPS()
{
if (updateClock.Throttling)
{
double newAimDrawFPS = drawClock.MaximumUpdateHz;
double newAimUpdateFPS = updateClock.MaximumUpdateHz;
if (aimDrawFPS != newAimDrawFPS || aimUpdateFPS != newAimUpdateFPS)
{
aimDrawFPS = newAimDrawFPS;
aimUpdateFPS = newAimUpdateFPS;
return true;
}
}
else
{
double newAimFPS = inputClock.MaximumUpdateHz;
if (aimDrawFPS != newAimFPS || aimUpdateFPS != newAimFPS)
{
aimUpdateFPS = aimDrawFPS = newAimFPS;
return true;
}
}
return false;
}
private ColourInfo getColour(double performanceRatio)
{
if (performanceRatio < 0.5f)
return Interpolation.ValueAt(performanceRatio, colours.Red, colours.Orange2, 0, 0.5);
return Interpolation.ValueAt(performanceRatio, colours.Orange2, colours.Lime0, 0.5, 0.9);
}
public ITooltip GetCustomTooltip() => new FPSCounterTooltip();
public object TooltipContent => this;
}
}

Some files were not shown because too many files have changed in this diff Show More