1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-05 05:52:56 +08:00

Merge branch 'master' into velocitychanges

This commit is contained in:
Dan Balasescu 2022-07-19 19:57:47 +09:00 committed by GitHub
commit cfb2adfd27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
173 changed files with 4403 additions and 2356 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.707.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.719.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

@ -14,6 +14,7 @@ using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Tournament; using osu.Game.Tournament;
using SDL2;
using Squirrel; using Squirrel;
namespace osu.Desktop namespace osu.Desktop
@ -29,7 +30,27 @@ namespace osu.Desktop
{ {
// run Squirrel first, as the app may exit after these run // run Squirrel first, as the app may exit after these run
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{
var windowsVersion = Environment.OSVersion.Version;
// While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
// See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
// disabling it ourselves.
// We could also better detect compatibility mode if required:
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!",
"This version of osu! requires at least Windows 8.1 to run.\n"
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
return;
}
setupSquirrel(); setupSquirrel();
}
// Back up the cwd before DesktopGameHost changes it // Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory; string cwd = Environment.CurrentDirectory;

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

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

@ -0,0 +1,175 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModSingleTap : OsuModTestScene
{
[Test]
public void TestInputSingular() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
new HitCircle
{
StartTime = 1500,
Position = new Vector2(300, 100),
},
new HitCircle
{
StartTime = 2000,
Position = new Vector2(400, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
new OsuReplayFrame(1001, new Vector2(200, 100)),
new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
new OsuReplayFrame(1501, new Vector2(300, 100)),
new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
new OsuReplayFrame(2001, new Vector2(400, 100)),
}
});
/// <summary>
/// Ensures singletapping is reset before the first hitobject after intro.
/// </summary>
[Test]
public void TestInputAlternatingAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
// first press during intro.
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
// press different key at hitobject and ensure it has been hit.
new OsuReplayFrame(1000, new Vector2(100), OsuAction.RightButton),
}
});
/// <summary>
/// Ensures singletapping is reset before the first hitobject after a break.
/// </summary>
[Test]
public void TestInputAlternatingWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(500, 2000),
},
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 2500,
Position = new Vector2(500, 100),
},
new HitCircle
{
StartTime = 3000,
Position = new Vector2(500, 100),
},
}
},
ReplayFrames = new List<ReplayFrame>
{
// first press to start singletap lock.
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
// press different key after break but before hit object.
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.RightButton),
new OsuReplayFrame(2251, new Vector2(300, 100)),
// press same key at second hitobject and ensure it has been hit.
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(500, 100)),
// press different key at third hitobject and ensure it has been missed.
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.RightButton),
new OsuReplayFrame(3001, new Vector2(500, 100)),
}
});
}
}

View File

@ -0,0 +1,114 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>
{
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
public override ModType Type => ModType.Conversion;
private const double flash_duration = 1000;
private DrawableRuleset<OsuHitObject> ruleset = null!;
protected OsuAction? LastAcceptedAction { get; private set; }
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods = null!;
private IFrameStableClock gameplayClock = null!;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
protected abstract bool CheckValidNewAction(OsuAction action);
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
LastAcceptedAction = null;
return true;
}
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (CheckValidNewAction(action))
{
LastAcceptedAction = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly InputBlockingMod mod;
public InputInterceptor(InputBlockingMod mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
}
}

View File

@ -1,119 +1,20 @@
// 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.Linq; using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject> public class OsuModAlternate : InputBlockingMod
{ {
public override string Name => @"Alternate"; public override string Name => @"Alternate";
public override string Acronym => @"AL"; public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!"; public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard; public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
private const double flash_duration = 1000; protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods;
private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset;
private IFrameStableClock gameplayClock;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
lastActionPressed = null;
return true;
}
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (lastActionPressed != action)
{
// User alternated correctly.
lastActionPressed = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly OsuModAlternate mod;
public InputInterceptor(OsuModAlternate mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
} }
} }

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<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

@ -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) }).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

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

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{ {
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAlternate) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
/// <summary> /// <summary>
/// How early before a hitobject's start time to trigger a hit. /// How early before a hitobject's start time to trigger a hit.

View File

@ -0,0 +1,18 @@
// 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.Linq;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSingleTap : InputBlockingMod
{
public override string Name => @"Single Tap";
public override string Acronym => @"SG";
public override string Description => @"You must only use one key!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
}
}

View File

@ -319,13 +319,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
const float fade_out_time = 450; const float fade_out_time = 450;
// intentionally pile on an extra FadeOut to make it happen much faster.
Ball.FadeOut(fade_out_time / 4, Easing.Out);
switch (state) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
if (SliderBody?.SnakingOut.Value == true) if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break; break;

View File

@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
{ {
public const float FOLLOW_AREA = 2.4f;
public Func<OsuAction?> GetInitialHitAction; public Func<OsuAction?> GetInitialHitAction;
public Color4 AccentColour public Color4 AccentColour
@ -31,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
set => ball.Colour = value; set => ball.Colour = value;
} }
private Drawable followCircle;
private Drawable followCircleReceptor; private Drawable followCircleReceptor;
private DrawableSlider drawableSlider; private DrawableSlider drawableSlider;
private Drawable ball; private Drawable ball;
@ -47,12 +48,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Children = new[] Children = new[]
{ {
followCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()) new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0,
}, },
followCircleReceptor = new CircularContainer followCircleReceptor = new CircularContainer
{ {
@ -103,10 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
tracking = value; tracking = value;
followCircleReceptor.Scale = new Vector2(tracking ? 2.4f : 1f); followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f);
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
} }
} }

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

@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(), new OsuModClassic(),
new OsuModRandom(), new OsuModRandom(),
new OsuModMirror(), new OsuModMirror(),
new OsuModAlternate(), new MultiMod(new OsuModAlternate(), new OsuModSingleTap())
}; };
case ModType.Automation: case ModType.Automation:

View File

@ -1,19 +1,19 @@
// 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 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.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
public class DefaultFollowCircle : CompositeDrawable public class DefaultFollowCircle : FollowCircle
{ {
public DefaultFollowCircle() public DefaultFollowCircle()
{ {
RelativeSizeAxes = Axes.Both;
InternalChild = new CircularContainer InternalChild = new CircularContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -29,5 +29,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
} }
}; };
} }
protected override void OnTrackingChanged(ValueChangedEvent<bool> tracking)
{
const float scale_duration = 300f;
const float fade_duration = 300f;
this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint)
.FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint);
}
protected override void OnSliderEnd()
{
const float fade_duration = 450f;
// intentionally pile on an extra FadeOut to make it happen much faster
this.FadeOut(fade_duration / 4, Easing.Out);
}
} }
} }

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.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -19,13 +17,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
public class DefaultSliderBall : CompositeDrawable public class DefaultSliderBall : CompositeDrawable
{ {
private Box box; private Box box = null!;
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin) private void load(ISkinSource skin)
{ {
var slider = (DrawableSlider)drawableObject;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
@ -51,10 +50,62 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
} }
}; };
if (parentObject != null)
{
var slider = (DrawableSlider)parentObject;
slider.Tracking.BindValueChanged(trackingChanged, true); slider.Tracking.BindValueChanged(trackingChanged, true);
} }
}
protected override void LoadComplete()
{
base.LoadComplete();
if (parentObject != null)
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value);
}
}
private void trackingChanged(ValueChangedEvent<bool> tracking) => private void trackingChanged(ValueChangedEvent<bool> tracking) =>
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint); box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
{
// Gets called by slider ticks, tails, etc., leading to duplicated
// animations which may negatively affect performance
if (drawableObject is not DrawableSlider)
return;
const float fade_duration = 450f;
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
{
this.FadeIn()
.ScaleTo(1f);
}
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
// intentionally pile on an extra FadeOut to make it happen much faster
this.FadeOut(fade_duration / 4, Easing.Out);
switch (state)
{
case ArmedState.Hit:
this.ScaleTo(1.4f, fade_duration, Easing.Out);
break;
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (parentObject != null)
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
}
} }
} }

View File

@ -0,0 +1,75 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract class FollowCircle : CompositeDrawable
{
[Resolved]
protected DrawableHitObject? ParentObject { get; private set; }
protected FollowCircle()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(OnTrackingChanged, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
if (ParentObject != null)
{
ParentObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(ParentObject);
ParentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(ParentObject, ParentObject.State.Value);
}
}
private void onHitObjectApplied(DrawableHitObject drawableObject)
{
this.ScaleTo(1f)
.FadeOut();
}
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
{
// Gets called by slider ticks, tails, etc., leading to duplicated
// animations which may negatively affect performance
if (drawableObject is not DrawableSlider)
return;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
OnSliderEnd();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (ParentObject != null)
{
ParentObject.HitObjectApplied -= onHitObjectApplied;
ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
protected abstract void OnTrackingChanged(ValueChangedEvent<bool> tracking);
protected abstract void OnSliderEnd();
}
}

View File

@ -1,12 +1,14 @@
// 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;
using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class LegacyFollowCircle : CompositeDrawable public class LegacyFollowCircle : FollowCircle
{ {
public LegacyFollowCircle(Drawable animationContent) public LegacyFollowCircle(Drawable animationContent)
{ {
@ -18,5 +20,36 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChild = animationContent; InternalChild = animationContent;
} }
protected override void OnTrackingChanged(ValueChangedEvent<bool> tracking)
{
Debug.Assert(ParentObject != null);
if (ParentObject.Judged)
return;
double remainingTime = Math.Max(0, ParentObject.HitStateUpdateTime - Time.Current);
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
if (tracking.NewValue)
{
// TODO: Follow circle should bounce on each slider tick.
this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
.FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
}
else
{
// TODO: Should animate only at the next slider tick if we want to match stable perfectly.
this.ScaleTo(4f, 100)
.FadeTo(0f, 100);
}
}
protected override void OnSliderEnd()
{
this.ScaleTo(1.6f, 200, Easing.Out)
.FadeOut(200, Easing.In);
}
} }
} }

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

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

@ -4,9 +4,12 @@
#nullable disable #nullable disable
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.NonVisual namespace osu.Game.Tests.NonVisual
{ {
@ -23,6 +26,83 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo)); Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
} }
[Test]
public void TestAudioEqualityNoFile()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test]
public void TestAudioEqualitySameHash()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
addAudioFile(beatmapSetA, "abc");
addAudioFile(beatmapSetB, "abc");
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test]
public void TestAudioEqualityDifferentHash()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
addAudioFile(beatmapSetA);
addAudioFile(beatmapSetB);
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test]
public void TestAudioEqualityBeatmapInfoSameHash()
{
var beatmapSet = TestResources.CreateTestBeatmapSetInfo(2);
addAudioFile(beatmapSet);
var beatmap1 = beatmapSet.Beatmaps.First();
var beatmap2 = beatmapSet.Beatmaps.Last();
Assert.AreNotEqual(beatmap1, beatmap2);
Assert.IsTrue(beatmap1.AudioEquals(beatmap2));
}
[Test]
public void TestAudioEqualityBeatmapInfoDifferentHash()
{
var beatmapSet = TestResources.CreateTestBeatmapSetInfo(2);
const string filename1 = "audio1.mp3";
const string filename2 = "audio2.mp3";
addAudioFile(beatmapSet, filename: filename1);
addAudioFile(beatmapSet, filename: filename2);
var beatmap1 = beatmapSet.Beatmaps.First();
var beatmap2 = beatmapSet.Beatmaps.Last();
Assert.AreNotEqual(beatmap1, beatmap2);
beatmap1.Metadata.AudioFile = filename1;
beatmap2.Metadata.AudioFile = filename2;
Assert.IsFalse(beatmap1.AudioEquals(beatmap2));
}
private static void addAudioFile(BeatmapSetInfo beatmapSetInfo, string hash = null, string filename = null)
{
beatmapSetInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = hash ?? Guid.NewGuid().ToString() }, filename ?? "audio.mp3"));
}
[Test] [Test]
public void TestDatabasedWithDatabased() public void TestDatabasedWithDatabased()
{ {

View File

@ -134,10 +134,11 @@ namespace osu.Game.Tests.Resources
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})", DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
StarRating = diff, StarRating = diff,
Length = length, Length = length,
BeatmapSet = beatmapSet,
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

@ -6,6 +6,7 @@ using System.IO;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -103,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
*/ */
public void TestAddAudioTrack() public void TestAddAudioTrack()
{ {
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
AddAssert("switch track to real track", () => AddAssert("switch track to real track", () =>
{ {
var setup = Editor.ChildrenOfType<SetupScreen>().First(); var setup = Editor.ChildrenOfType<SetupScreen>().First();
@ -131,6 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
}); });
AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
} }

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

@ -1,20 +1,25 @@
// 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;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; 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;
@ -57,13 +62,46 @@ 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]
public void TestLastPlayedUpdated()
{
DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
AddAssert("last played is null", () => getLastPlayed() == null);
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
}
[Test] [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

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@ -195,12 +196,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestDownloadButtonHiddenWhenBeatmapExists() public void TestDownloadButtonHiddenWhenBeatmapExists()
{ {
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
Live<BeatmapSetInfo> imported = null; Live<BeatmapSetInfo> imported = null;
Debug.Assert(beatmap.BeatmapSet != null); AddStep("import beatmap", () =>
{
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet)); Debug.Assert(beatmap.BeatmapSet != null);
imported = manager.Import(beatmap.BeatmapSet);
});
createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach())); createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach()));
@ -245,14 +249,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestExpiredItems() public void TestExpiredItems()
{ {
AddStep("create playlist", () => createPlaylist(p =>
{ {
Child = playlist = new TestPlaylist p.Items.Clear();
{ p.Items.AddRange(new[]
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300),
Items =
{ {
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{ {
@ -277,8 +277,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
new APIMod(new OsuModAutoplay()) new APIMod(new OsuModAutoplay())
} }
} }
} });
};
}); });
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
@ -321,19 +320,44 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(2 + index * 2).Alpha > 0) == visible); () => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(2 + index * 2).Alpha > 0) == visible);
private void createPlaylistWithBeatmaps(Func<IEnumerable<IBeatmapInfo>> beatmaps) => createPlaylist(p =>
{
int index = 0;
p.Items.Clear();
foreach (var b in beatmaps())
{
p.Items.Add(new PlaylistItem(b)
{
ID = index++,
OwnerID = 2,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
new APIMod(new OsuModHardRock()),
new APIMod(new OsuModDoubleTime()),
new APIMod(new OsuModAutoplay())
}
});
}
});
private void createPlaylist(Action<TestPlaylist> setupPlaylist = null) private void createPlaylist(Action<TestPlaylist> setupPlaylist = null)
{ {
AddStep("create playlist", () => AddStep("create playlist", () =>
{ {
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = playlist = new TestPlaylist Child = playlist = new TestPlaylist
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(500, 300) Size = new Vector2(500, 300)
}
}; };
setupPlaylist?.Invoke(playlist);
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
{ {
playlist.Items.Add(new PlaylistItem(i % 2 == 1 playlist.Items.Add(new PlaylistItem(i % 2 == 1
@ -360,39 +384,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
}); });
} }
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); setupPlaylist?.Invoke(playlist);
}
private void createPlaylistWithBeatmaps(Func<IEnumerable<IBeatmapInfo>> beatmaps)
{
AddStep("create playlist", () =>
{
Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300)
};
int index = 0;
foreach (var b in beatmaps())
{
playlist.Items.Add(new PlaylistItem(b)
{
ID = index++,
OwnerID = 2,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
new APIMod(new OsuModHardRock()),
new APIMod(new OsuModDoubleTime()),
new APIMod(new OsuModAutoplay())
}
});
}
}); });
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -368,12 +369,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
ParticipantsList? participantsList = null; ParticipantsList? participantsList = null;
AddStep("create new list", () => Child = participantsList = new ParticipantsList AddStep("create new list", () => Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = participantsList = new ParticipantsList
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f) Size = new Vector2(380, 0.7f)
}
}); });
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true); AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);

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

@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Graphics; using osuTK.Graphics;
@ -146,21 +147,17 @@ namespace osu.Game.Tests.Visual.Online
{ {
var scores = new APIScoresCollection var scores = new APIScoresCollection
{ {
Scores = new List<APIScore> Scores = new List<SoloScoreInfo>
{ {
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 6602580, Id = 6602580,
Username = @"waaiiru", Username = @"waaiiru",
Country = new Country CountryCode = CountryCode.ES,
{
FullName = @"Spain",
FlagName = @"ES",
},
}, },
Mods = new[] Mods = new[]
{ {
@ -175,19 +172,15 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890, TotalScore = 1234567890,
Accuracy = 1, Accuracy = 1,
}, },
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 4608074, Id = 4608074,
Username = @"Skycries", Username = @"Skycries",
Country = new Country CountryCode = CountryCode.BR,
{
FullName = @"Brazil",
FlagName = @"BR",
},
}, },
Mods = new[] Mods = new[]
{ {
@ -201,19 +194,15 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789, TotalScore = 1234789,
Accuracy = 0.9997, Accuracy = 0.9997,
}, },
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 1014222, Id = 1014222,
Username = @"eLy", Username = @"eLy",
Country = new Country CountryCode = CountryCode.JP,
{
FullName = @"Japan",
FlagName = @"JP",
},
}, },
Mods = new[] Mods = new[]
{ {
@ -226,19 +215,15 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678, TotalScore = 12345678,
Accuracy = 0.9854, Accuracy = 0.9854,
}, },
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 1541390, Id = 1541390,
Username = @"Toukai", Username = @"Toukai",
Country = new Country CountryCode = CountryCode.CA,
{
FullName = @"Canada",
FlagName = @"CA",
},
}, },
Mods = new[] Mods = new[]
{ {
@ -250,19 +235,15 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567, TotalScore = 1234567,
Accuracy = 0.8765, Accuracy = 0.8765,
}, },
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
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,
@ -273,14 +254,18 @@ namespace osu.Game.Tests.Visual.Online
} }
}; };
const int initial_great_count = 2000;
int greatCount = initial_great_count;
foreach (var s in scores.Scores) foreach (var s in scores.Scores)
{ {
s.Statistics = new Dictionary<string, int> s.Statistics = new Dictionary<HitResult, int>
{ {
{ "count_300", RNG.Next(2000) }, { HitResult.Great, greatCount -= 100 },
{ "count_100", RNG.Next(2000) }, { HitResult.Ok, RNG.Next(100) },
{ "count_50", RNG.Next(2000) }, { HitResult.Meh, RNG.Next(100) },
{ "count_miss", RNG.Next(2000) } { HitResult.Miss, initial_great_count - greatCount }
}; };
} }
@ -289,19 +274,15 @@ namespace osu.Game.Tests.Visual.Online
private APIScoreWithPosition createUserBest() => new APIScoreWithPosition private APIScoreWithPosition createUserBest() => new APIScoreWithPosition
{ {
Score = new APIScore Score = new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
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
}; };

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

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

@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Components
{ {
public class TournamentSpriteTextWithBackground : CompositeDrawable public class TournamentSpriteTextWithBackground : CompositeDrawable
{ {
protected readonly TournamentSpriteText Text; public readonly TournamentSpriteText Text;
protected readonly Box Background; protected readonly Box Background;
public TournamentSpriteTextWithBackground(string text = "") public TournamentSpriteTextWithBackground(string text = "")

View File

@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Components
private Video video; private Video video;
private ManualClock manualClock; private ManualClock manualClock;
public bool VideoAvailable => video != null;
public TourneyVideo(string filename, bool drawFallbackGradient = false) public TourneyVideo(string filename, bool drawFallbackGradient = false)
{ {
this.filename = filename; this.filename = filename;

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

@ -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 Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Models
public int ID; public int ID;
[JsonProperty("BeatmapInfo")] [JsonProperty("BeatmapInfo")]
public TournamentBeatmap Beatmap; public TournamentBeatmap? Beatmap;
public long Score; public long Score;

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

@ -0,0 +1,101 @@
// 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.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Tournament
{
internal class SaveChangesOverlay : CompositeDrawable
{
[Resolved]
private TournamentGame tournamentGame { get; set; } = null!;
private string? lastSerialisedLadder;
private readonly TourneyButton saveChangesButton;
public SaveChangesOverlay()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new Container
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Position = new Vector2(5),
CornerRadius = 10,
Masking = true,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.2f),
RelativeSizeAxes = Axes.Both,
},
saveChangesButton = new TourneyButton
{
Text = "Save Changes",
Width = 140,
Height = 50,
Padding = new MarginPadding
{
Top = 10,
Left = 10,
},
Margin = new MarginPadding
{
Right = 10,
Bottom = 10,
},
Action = saveChanges,
// Enabled = { Value = false },
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
scheduleNextCheck();
}
private async Task checkForChanges()
{
string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder());
// If a save hasn't been triggered by the user yet, populate the initial value
lastSerialisedLadder ??= serialisedLadder;
if (lastSerialisedLadder != serialisedLadder && !saveChangesButton.Enabled.Value)
{
saveChangesButton.Enabled.Value = true;
saveChangesButton.Background
.FadeColour(saveChangesButton.BackgroundColour.Lighten(0.5f), 500, Easing.In).Then()
.FadeColour(saveChangesButton.BackgroundColour, 500, Easing.Out)
.Loop();
}
scheduleNextCheck();
}
private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
private void saveChanges()
{
tournamentGame.SaveChanges();
lastSerialisedLadder = tournamentGame.GetSerialisedLadder();
saveChangesButton.Enabled.Value = false;
saveChangesButton.Background.FadeColour(saveChangesButton.BackgroundColour, 500);
}
}
}

View File

@ -12,8 +12,6 @@ using osu.Framework.Allocation;
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.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -45,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Drawings
public ITeamList TeamList; public ITeamList TeamList;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(TextureStore textures, Storage storage) private void load(Storage storage)
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -91,11 +89,10 @@ namespace osu.Game.Tournament.Screens.Drawings
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
new Sprite new TourneyVideo("drawings")
{ {
Loop = true,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Texture = textures.Get(@"Backgrounds/Drawings/background.png")
}, },
// Visualiser // Visualiser
new VisualiserContainer new VisualiserContainer

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);
@ -298,10 +301,10 @@ namespace osu.Game.Tournament.Screens.Editors
}, true); }, true);
} }
private void updatePanel() private void updatePanel() => Scheduler.AddOnce(() =>
{ {
drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 }; drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 };
} });
} }
} }
} }

View File

@ -20,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Editors namespace osu.Game.Tournament.Screens.Editors
{ {
public abstract class TournamentEditorScreen<TDrawable, TModel> : TournamentScreen, IProvideVideo public abstract class TournamentEditorScreen<TDrawable, TModel> : TournamentScreen
where TDrawable : Drawable, IModelBacked<TModel> where TDrawable : Drawable, IModelBacked<TModel>
where TModel : class, new() where TModel : class, new()
{ {

View File

@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{ {
private readonly TeamScore score; private readonly TeamScore score;
private readonly TournamentSpriteTextWithBackground teamText;
private readonly Bindable<string> teamName = new Bindable<string>("???");
private bool showScore; private bool showScore;
public bool ShowScore public bool ShowScore
@ -93,7 +97,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
} }
} }
}, },
new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???") teamText = new TournamentSpriteTextWithBackground
{ {
Scale = new Vector2(0.5f), Scale = new Vector2(0.5f),
Origin = anchor, Origin = anchor,
@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
updateDisplay(); updateDisplay();
FinishTransforms(true); FinishTransforms(true);
if (Team != null)
teamName.BindTo(Team.FullName);
teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
} }
private void updateDisplay() private void updateDisplay()

View File

@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
currentMatch.BindTo(ladder.CurrentMatch); currentMatch.BindTo(ladder.CurrentMatch);
currentMatch.BindValueChanged(matchChanged); currentMatch.BindValueChanged(matchChanged);
currentTeam.BindValueChanged(teamChanged);
updateMatch(); updateMatch();
} }
@ -67,7 +69,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
// team may change to same team, which means score is not in a good state. // team may change to same team, which means score is not in a good state.
// thus we handle this manually. // thus we handle this manually.
teamChanged(currentTeam.Value); currentTeam.TriggerChange();
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
private void teamChanged(TournamentTeam team) private void teamChanged(ValueChangedEvent<TournamentTeam> team)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
}; };
} }
} }

View File

@ -21,7 +21,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Gameplay namespace osu.Game.Tournament.Screens.Gameplay
{ {
public class GameplayScreen : BeatmapInfoScreen, IProvideVideo public class GameplayScreen : BeatmapInfoScreen
{ {
private readonly BindableBool warmup = new BindableBool(); private readonly BindableBool warmup = new BindableBool();

View File

@ -1,14 +0,0 @@
// 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
namespace osu.Game.Tournament.Screens
{
/// <summary>
/// Marker interface for a screen which provides its own local video background.
/// </summary>
public interface IProvideVideo
{
}
}

View File

@ -53,6 +53,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection => editorInfo.Selected.ValueChanged += selection =>
{ {
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
GetContainingInputManager().TriggerFocusContention(null);
roundDropdown.Current = selection.NewValue?.Round; roundDropdown.Current = selection.NewValue?.Round;
losersCheckbox.Current = selection.NewValue?.Losers; losersCheckbox.Current = selection.NewValue?.Losers;
dateTimeBox.Current = selection.NewValue?.Date; dateTimeBox.Current = selection.NewValue?.Date;

View File

@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Ladder namespace osu.Game.Tournament.Screens.Ladder
{ {
public class LadderScreen : TournamentScreen, IProvideVideo public class LadderScreen : TournamentScreen
{ {
protected Container<DrawableTournamentMatch> MatchesContainer; protected Container<DrawableTournamentMatch> MatchesContainer;
private Container<Path> paths; private Container<Path> paths;

View File

@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Schedule namespace osu.Game.Tournament.Screens.Schedule
{ {
public class ScheduleScreen : TournamentScreen // IProvidesVideo public class ScheduleScreen : TournamentScreen
{ {
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>(); private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
private Container mainContainer; private Container mainContainer;

View File

@ -9,6 +9,8 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
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.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -19,7 +21,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Setup namespace osu.Game.Tournament.Screens.Setup
{ {
public class SetupScreen : TournamentScreen, IProvideVideo public class SetupScreen : TournamentScreen
{ {
private FillFlowContainer fillFlow; private FillFlowContainer fillFlow;
@ -48,13 +50,21 @@ namespace osu.Game.Tournament.Screens.Setup
{ {
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize); windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
InternalChild = fillFlow = new FillFlowContainer InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f),
},
fillFlow = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Padding = new MarginPadding(10), Padding = new MarginPadding(10),
Spacing = new Vector2(10), Spacing = new Vector2(10),
}
}; };
api.LocalUser.BindValueChanged(_ => Schedule(reload)); api.LocalUser.BindValueChanged(_ => Schedule(reload));
@ -74,7 +84,8 @@ namespace osu.Game.Tournament.Screens.Setup
Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()), Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()),
Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found",
Failing = fileBasedIpc?.IPCStorage == null, Failing = fileBasedIpc?.IPCStorage == null,
Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation." Description =
"The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
}, },
new ActionableInfo new ActionableInfo
{ {

View File

@ -14,7 +14,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Showcase namespace osu.Game.Tournament.Screens.Showcase
{ {
public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo public class ShowcaseScreen : BeatmapInfoScreen
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -19,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro namespace osu.Game.Tournament.Screens.TeamIntro
{ {
public class SeedingScreen : TournamentMatchScreen, IProvideVideo public class SeedingScreen : TournamentMatchScreen
{ {
private Container mainContainer; private Container mainContainer;
@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
currentTeam.BindValueChanged(teamChanged, true); currentTeam.BindValueChanged(teamChanged, true);
} }
private void teamChanged(ValueChangedEvent<TournamentTeam> team) private void teamChanged(ValueChangedEvent<TournamentTeam> team) => Scheduler.AddOnce(() =>
{ {
if (team.NewValue == null) if (team.NewValue == null)
{ {
@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
} }
showTeam(team.NewValue); showTeam(team.NewValue);
} });
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match) protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
{ {
@ -120,15 +121,23 @@ namespace osu.Game.Tournament.Screens.TeamIntro
foreach (var seeding in team.SeedingResults) foreach (var seeding in team.SeedingResults)
{ {
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value)); fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
foreach (var beatmap in seeding.Beatmaps) foreach (var beatmap in seeding.Beatmaps)
{
if (beatmap.Beatmap == null)
continue;
fill.Add(new BeatmapScoreRow(beatmap)); fill.Add(new BeatmapScoreRow(beatmap));
} }
} }
}
private class BeatmapScoreRow : CompositeDrawable private class BeatmapScoreRow : CompositeDrawable
{ {
public BeatmapScoreRow(SeedingBeatmap beatmap) public BeatmapScoreRow(SeedingBeatmap beatmap)
{ {
Debug.Assert(beatmap.Beatmap != null);
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Children = new Drawable[] Children = new Drawable[]
{ {
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 }, new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, new TournamentSpriteText
{ Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
} }
}, },
}; };

View File

@ -13,7 +13,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro namespace osu.Game.Tournament.Screens.TeamIntro
{ {
public class TeamIntroScreen : TournamentMatchScreen, IProvideVideo public class TeamIntroScreen : TournamentMatchScreen
{ {
private Container mainContainer; private Container mainContainer;

View File

@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamWin namespace osu.Game.Tournament.Screens.TeamWin
{ {
public class TeamWinScreen : TournamentMatchScreen, IProvideVideo public class TeamWinScreen : TournamentMatchScreen
{ {
private Container mainContainer; private Container mainContainer;
@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
private bool firstDisplay = true; private bool firstDisplay = true;
private void update() => Schedule(() => private void update() => Scheduler.AddOnce(() =>
{ {
var match = CurrentMatch.Value; var match = CurrentMatch.Value;

View File

@ -11,8 +11,6 @@ using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -20,11 +18,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tournament namespace osu.Game.Tournament
{ {
[Cached]
public class TournamentGame : TournamentGameBase public class TournamentGame : TournamentGameBase
{ {
public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE;
@ -78,40 +76,9 @@ namespace osu.Game.Tournament
LoadComponentsAsync(new[] LoadComponentsAsync(new[]
{ {
new Container new SaveChangesOverlay
{ {
CornerRadius = 10,
Depth = float.MinValue, Depth = float.MinValue,
Position = new Vector2(5),
Masking = true,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.2f),
RelativeSizeAxes = Axes.Both,
},
new TourneyButton
{
Text = "Save Changes",
Width = 140,
Height = 50,
Padding = new MarginPadding
{
Top = 10,
Left = 10,
},
Margin = new MarginPadding
{
Right = 10,
Bottom = 10,
},
Action = SaveChanges,
},
}
}, },
heightWarning = new WarningBox("Please make the window wider") heightWarning = new WarningBox("Please make the window wider")
{ {

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,14 +291,14 @@ 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();
} }
} }
protected virtual void SaveChanges() public void SaveChanges()
{ {
if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully) if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)
{ {
@ -311,7 +314,16 @@ namespace osu.Game.Tournament
.ToList(); .ToList();
// Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state. // Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state.
string serialisedLadder = JsonConvert.SerializeObject(ladder, string serialisedLadder = GetSerialisedLadder();
using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
using (var sw = new StreamWriter(stream))
sw.Write(serialisedLadder);
}
public string GetSerialisedLadder()
{
return JsonConvert.SerializeObject(ladder,
new JsonSerializerSettings new JsonSerializerSettings
{ {
Formatting = Formatting.Indented, Formatting = Formatting.Indented,
@ -319,10 +331,6 @@ namespace osu.Game.Tournament
DefaultValueHandling = DefaultValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() } Converters = new JsonConverter[] { new JsonPointConverter() }
}); });
using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
using (var sw = new StreamWriter(stream))
sw.Write(serialisedLadder);
} }
protected override UserInputManager CreateUserInputManager() => new TournamentInputManager(); protected override UserInputManager CreateUserInputManager() => new TournamentInputManager();

View File

@ -10,6 +10,7 @@ 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.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -186,7 +187,7 @@ namespace osu.Game.Tournament
var lastScreen = currentScreen; var lastScreen = currentScreen;
currentScreen = target; currentScreen = target;
if (currentScreen is IProvideVideo) if (currentScreen.ChildrenOfType<TourneyVideo>().FirstOrDefault()?.VideoAvailable == true)
{ {
video.FadeOut(200); video.FadeOut(200);

View File

@ -3,12 +3,15 @@
#nullable disable #nullable disable
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tournament namespace osu.Game.Tournament
{ {
public class TourneyButton : OsuButton public class TourneyButton : OsuButton
{ {
public new Box Background => base.Background;
public TourneyButton() public TourneyButton()
: base(null) : base(null)
{ {

View File

@ -80,9 +80,8 @@ namespace osu.Game.Beatmaps
if (beatmapSet.OnlineID > 0) if (beatmapSet.OnlineID > 0)
{ {
var existingSetWithSameOnlineID = realm.All<BeatmapSetInfo>().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure.
foreach (var existingSetWithSameOnlineID in realm.All<BeatmapSetInfo>().Where(b => b.OnlineID == beatmapSet.OnlineID))
if (existingSetWithSameOnlineID != null)
{ {
existingSetWithSameOnlineID.DeletePending = true; existingSetWithSameOnlineID.DeletePending = true;
existingSetWithSameOnlineID.OnlineID = -1; existingSetWithSameOnlineID.OnlineID = -1;
@ -90,7 +89,7 @@ namespace osu.Game.Beatmaps
foreach (var b in existingSetWithSameOnlineID.Beatmaps) foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineID = -1; b.OnlineID = -1;
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted."); LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion.");
} }
} }
} }

View File

@ -2,6 +2,7 @@
// 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; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
public bool SamplesMatchPlaybackRate { get; set; } = true; public bool SamplesMatchPlaybackRate { get; set; } = true;
/// <summary>
/// The time at which this beatmap was last played by the local user.
/// </summary>
public DateTimeOffset? LastPlayed { get; set; }
/// <summary> /// <summary>
/// The ratio of distance travelled per time unit. /// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>). /// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
@ -151,14 +157,23 @@ namespace osu.Game.Beatmaps
public bool AudioEquals(BeatmapInfo? other) => other != null public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null && BeatmapSet != null
&& other.BeatmapSet != null && other.BeatmapSet != null
&& BeatmapSet.Hash == other.BeatmapSet.Hash && compareFiles(this, other, m => m.AudioFile);
&& Metadata.AudioFile == other.Metadata.AudioFile;
public bool BackgroundEquals(BeatmapInfo? other) => other != null public bool BackgroundEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null && BeatmapSet != null
&& other.BeatmapSet != null && other.BeatmapSet != null
&& BeatmapSet.Hash == other.BeatmapSet.Hash && compareFiles(this, other, m => m.BackgroundFile);
&& Metadata.BackgroundFile == other.Metadata.BackgroundFile;
private static bool compareFiles(BeatmapInfo x, BeatmapInfo y, Func<IBeatmapMetadataInfo, string> getFilename)
{
Debug.Assert(x.BeatmapSet != null);
Debug.Assert(y.BeatmapSet != null);
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash;
return fileHashX == fileHashY;
}
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;

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

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

@ -0,0 +1,23 @@
// 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.Beatmaps;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Collections
{
public class CollectionToggleMenuItem : ToggleMenuItem
{
public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap)
: base(collection.Name.Value, MenuItemType.Standard, state =>
{
if (state)
collection.BeatmapHashes.Add(beatmap.MD5Hash);
else
collection.BeatmapHashes.Remove(beatmap.MD5Hash);
})
{
State.Value = collection.BeatmapHashes.Contains(beatmap.MD5Hash);
}
}
}

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

@ -58,8 +58,11 @@ namespace osu.Game.Database
/// 12 2021-11-24 Add Status to RealmBeatmapSet. /// 12 2021-11-24 Add Status to RealmBeatmapSet.
/// 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.
/// 16 2022-07-15 Removed HasReplay from ScoreInfo.
/// 17 2022-07-16 Added CountryCode to RealmUser.
/// </summary> /// </summary>
private const int schema_version = 14; private const int schema_version = 17;
/// <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

@ -94,6 +94,8 @@ namespace osu.Game.IO
error = OsuStorageError.None; error = OsuStorageError.None;
Storage lastStorage = UnderlyingStorage; Storage lastStorage = UnderlyingStorage;
Logger.Log($"Attempting to use custom storage location {CustomStoragePath}");
try try
{ {
Storage userStorage = host.GetStorage(CustomStoragePath); Storage userStorage = host.GetStorage(CustomStoragePath);
@ -102,6 +104,7 @@ namespace osu.Game.IO
error = OsuStorageError.AccessibleButEmpty; error = OsuStorageError.AccessibleButEmpty;
ChangeTargetStorage(userStorage); ChangeTargetStorage(userStorage);
Logger.Log($"Storage successfully changed to {CustomStoragePath}.");
} }
catch catch
{ {
@ -109,6 +112,9 @@ namespace osu.Game.IO
ChangeTargetStorage(lastStorage); ChangeTargetStorage(lastStorage);
} }
if (error != OsuStorageError.None)
Logger.Log($"Custom storage location could not be used ({error}).");
return error == OsuStorageError.None; return error == OsuStorageError.None;
} }

View File

@ -3,8 +3,8 @@
#nullable disable #nullable disable
using Humanizer;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using osu.Game.Extensions;
namespace osu.Game.IO.Serialization namespace osu.Game.IO.Serialization
{ {
@ -12,7 +12,7 @@ namespace osu.Game.IO.Serialization
{ {
protected override string ResolvePropertyName(string propertyName) protected override string ResolvePropertyName(string propertyName)
{ {
return propertyName.Underscore(); return propertyName.ToSnakeCase();
} }
} }
} }

View File

@ -2,9 +2,11 @@
// 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.ComponentModel; using System.ComponentModel;
using JetBrains.Annotations;
namespace osu.Game.Localisation namespace osu.Game.Localisation
{ {
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public enum Language public enum Language
{ {
[Description(@"English")] [Description(@"English")]

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 osu.Game.Database; using osu.Game.Database;
using osu.Game.Users; using osu.Game.Users;
@ -17,6 +15,16 @@ namespace osu.Game.Models
public string Username { get; set; } = string.Empty; public string Username { get; set; } = string.Empty;
[Ignored]
public CountryCode CountryCode
{
get => Enum.TryParse(CountryString, out CountryCode country) ? country : CountryCode.Unknown;
set => CountryString = value.ToString();
}
[MapTo(nameof(CountryCode))]
public string CountryString { get; set; } = default(CountryCode).ToString();
public bool IsBot => false; public bool IsBot => false;
public bool Equals(RealmUser other) public bool Equals(RealmUser other)

View File

@ -38,7 +38,7 @@ namespace osu.Game.Online.API
public string WebsiteRootUrl { get; } public string WebsiteRootUrl { get; }
public int APIVersion => 20220217; // We may want to pull this from the game version eventually. public int APIVersion => 20220705; // We may want to pull this from the game version eventually.
public Exception LastLoginError { get; private set; } public Exception LastLoginError { get; private set; }
@ -163,7 +163,13 @@ namespace osu.Game.Online.API
userReq.Failure += ex => userReq.Failure += ex =>
{ {
if (ex is WebException webException && webException.Message == @"Unauthorized") if (ex is APIException)
{
LastLoginError = ex;
log.Add("Login failed on local user retrieval!");
Logout();
}
else if (ex is WebException webException && webException.Message == @"Unauthorized")
{ {
log.Add(@"Login no longer valid"); log.Add(@"Login no longer valid");
Logout(); Logout();

View File

@ -5,13 +5,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Humanizer;
using MessagePack; using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -44,11 +45,11 @@ namespace osu.Game.Online.API
var bindable = (IBindable)property.GetValue(mod); var bindable = (IBindable)property.GetValue(mod);
if (!bindable.IsDefault) if (!bindable.IsDefault)
Settings.Add(property.Name.Underscore(), bindable.GetUnderlyingSettingValue()); Settings.Add(property.Name.ToSnakeCase(), bindable.GetUnderlyingSettingValue());
} }
} }
public Mod ToMod(Ruleset ruleset) public Mod ToMod([NotNull] Ruleset ruleset)
{ {
Mod resultMod = ruleset.CreateModFromAcronym(Acronym); Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
@ -62,11 +63,18 @@ namespace osu.Game.Online.API
{ {
foreach (var (_, property) in resultMod.GetSettingsSourceProperties()) foreach (var (_, property) in resultMod.GetSettingsSourceProperties())
{ {
if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue)) if (!Settings.TryGetValue(property.Name.ToSnakeCase(), out object settingValue))
continue; continue;
try
{
resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue); resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
} }
catch (Exception ex)
{
Logger.Log($"Failed to copy mod setting value '{settingValue ?? "null"}' to \"{property.Name}\": {ex.Message}");
}
}
} }
return resultMod; return resultMod;

View File

@ -4,7 +4,7 @@
#nullable disable #nullable disable
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using Humanizer; using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Comments; using osu.Game.Overlays.Comments;
@ -32,7 +32,7 @@ namespace osu.Game.Online.API.Requests
var req = base.CreateWebRequest(); var req = base.CreateWebRequest();
req.AddParameter("commentable_id", commentableId.ToString()); req.AddParameter("commentable_id", commentableId.ToString());
req.AddParameter("commentable_type", type.ToString().Underscore().ToLowerInvariant()); req.AddParameter("commentable_type", type.ToString().ToSnakeCase().ToLowerInvariant());
req.AddParameter("page", page.ToString()); req.AddParameter("page", page.ToString());
req.AddParameter("sort", sort.ToString().ToLowerInvariant()); req.AddParameter("sort", sort.ToString().ToLowerInvariant());

View File

@ -35,7 +35,7 @@ namespace osu.Game.Online.API.Requests
this.mods = mods ?? Array.Empty<IMod>(); this.mods = mods ?? Array.Empty<IMod>();
} }
protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/scores{createQueryParameters()}"; protected override string Target => $@"beatmaps/{beatmapInfo.OnlineID}/solo-scores{createQueryParameters()}";
private string createQueryParameters() private string createQueryParameters()
{ {

View File

@ -3,8 +3,8 @@
#nullable disable #nullable disable
using Humanizer;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests
@ -22,7 +22,7 @@ namespace osu.Game.Online.API.Requests
this.type = type; this.type = type;
} }
protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().Underscore()}"; protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().ToSnakeCase()}";
} }
public enum BeatmapSetType public enum BeatmapSetType

View File

@ -5,6 +5,7 @@
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests
{ {
@ -12,21 +13,21 @@ namespace osu.Game.Online.API.Requests
{ {
public readonly UserRankingsType Type; public readonly UserRankingsType Type;
private readonly string country; private readonly CountryCode countryCode;
public GetUserRankingsRequest(RulesetInfo ruleset, UserRankingsType type = UserRankingsType.Performance, int page = 1, string country = null) public GetUserRankingsRequest(RulesetInfo ruleset, UserRankingsType type = UserRankingsType.Performance, int page = 1, CountryCode countryCode = CountryCode.Unknown)
: base(ruleset, page) : base(ruleset, page)
{ {
Type = type; Type = type;
this.country = country; this.countryCode = countryCode;
} }
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()
{ {
var req = base.CreateWebRequest(); var req = base.CreateWebRequest();
if (country != null) if (countryCode != CountryCode.Unknown)
req.AddParameter("country", country); req.AddParameter("country", countryCode.ToString());
return req; return req;
} }

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests
{ {
public class GetUserScoresRequest : PaginatedAPIRequest<List<APIScore>> public class GetUserScoresRequest : PaginatedAPIRequest<List<SoloScoreInfo>>
{ {
private readonly long userId; private readonly long userId;
private readonly ScoreType type; private readonly ScoreType type;

View File

@ -4,8 +4,8 @@
#nullable disable #nullable disable
using System; using System;
using Humanizer;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Extensions;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses namespace osu.Game.Online.API.Requests.Responses
@ -21,7 +21,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty] [JsonProperty]
private string type private string type
{ {
set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize()); set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.ToPascalCase());
} }
public RecentActivityType Type; public RecentActivityType Type;

View File

@ -16,11 +16,11 @@ namespace osu.Game.Online.API.Requests.Responses
public int? Position; public int? Position;
[JsonProperty(@"score")] [JsonProperty(@"score")]
public APIScore Score; public SoloScoreInfo Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null) public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{ {
var score = Score.CreateScoreInfo(rulesets, beatmap); var score = Score.ToScoreInfo(rulesets, beatmap);
score.Position = Position; score.Position = Position;
return score; return score;
} }

View File

@ -11,9 +11,9 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIScoresCollection public class APIScoresCollection
{ {
[JsonProperty(@"scores")] [JsonProperty(@"scores")]
public List<APIScore> Scores; public List<SoloScoreInfo> Scores;
[JsonProperty(@"userScore")] [JsonProperty(@"user_score")]
public APIScoreWithPosition UserScore; public APIScoreWithPosition UserScore;
} }
} }

View File

@ -34,8 +34,19 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"previous_usernames")] [JsonProperty(@"previous_usernames")]
public string[] PreviousUsernames; public string[] PreviousUsernames;
private CountryCode? countryCode;
public CountryCode CountryCode
{
get => countryCode ??= (Enum.TryParse(country?.Code, out CountryCode result) ? result : default);
set => countryCode = value;
}
#pragma warning disable 649
[CanBeNull]
[JsonProperty(@"country")] [JsonProperty(@"country")]
public Country Country; private Country country;
#pragma warning restore 649
public readonly Bindable<UserStatus> Status = new Bindable<UserStatus>(); public readonly Bindable<UserStatus> Status = new Bindable<UserStatus>();
@ -256,5 +267,13 @@ namespace osu.Game.Online.API.Requests.Responses
public int OnlineID => Id; public int OnlineID => Id;
public bool Equals(APIUser other) => this.MatchesOnlineID(other); public bool Equals(APIUser other) => this.MatchesOnlineID(other);
#pragma warning disable 649
private class Country
{
[JsonProperty(@"code")]
public string Code;
}
#pragma warning restore 649
} }
} }

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 Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
{
[Serializable]
public class SoloScoreInfo : IHasOnlineID<long>
{
[JsonProperty("replay")]
public bool HasReplay { get; set; }
[JsonProperty("beatmap_id")]
public int BeatmapID { get; set; }
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
[JsonProperty("build_id")]
public int? BuildID { get; set; }
[JsonProperty("passed")]
public bool Passed { get; set; }
[JsonProperty("total_score")]
public int TotalScore { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("user_id")]
public int UserID { get; set; }
// TODO: probably want to update this column to match user stats (short)?
[JsonProperty("max_combo")]
public int MaxCombo { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("rank")]
public ScoreRank Rank { get; set; }
[JsonProperty("started_at")]
public DateTimeOffset? StartedAt { get; set; }
[JsonProperty("ended_at")]
public DateTimeOffset EndedAt { get; set; }
[JsonProperty("mods")]
public APIMod[] Mods { get; set; } = Array.Empty<APIMod>();
[JsonIgnore]
[JsonProperty("created_at")]
public DateTimeOffset CreatedAt { get; set; }
[JsonIgnore]
[JsonProperty("updated_at")]
public DateTimeOffset UpdatedAt { get; set; }
[JsonIgnore]
[JsonProperty("deleted_at")]
public DateTimeOffset? DeletedAt { get; set; }
[JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
#region osu-web API additions (not stored to database).
[JsonProperty("id")]
public long? ID { get; set; }
[JsonProperty("user")]
public APIUser? User { get; set; }
[JsonProperty("beatmap")]
public APIBeatmap? Beatmap { get; set; }
[JsonProperty("beatmapset")]
public APIBeatmapSet? BeatmapSet
{
set
{
// in the deserialisation case we need to ferry this data across.
// the order of properties returned by the API guarantees that the beatmap is populated by this point.
if (!(Beatmap is APIBeatmap apiBeatmap))
throw new InvalidOperationException("Beatmap set metadata arrived before beatmap metadata in response");
apiBeatmap.BeatmapSet = value;
}
}
[JsonProperty("pp")]
public double? PP { get; set; }
#endregion
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
/// <summary>
/// Create a <see cref="ScoreInfo"/> from an API score instance.
/// </summary>
/// <param name="rulesets">A ruleset store, used to populate a ruleset instance in the returned score.</param>
/// <param name="beatmap">An optional beatmap, copied into the returned score (for cases where the API does not populate the beatmap).</param>
/// <returns></returns>
public ScoreInfo ToScoreInfo(RulesetStore rulesets, BeatmapInfo? beatmap = null)
{
var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally");
var rulesetInstance = ruleset.CreateInstance();
var mods = Mods.Select(apiMod => apiMod.ToMod(rulesetInstance)).ToArray();
var scoreInfo = ToScoreInfo(mods);
scoreInfo.Ruleset = ruleset;
if (beatmap != null) scoreInfo.BeatmapInfo = beatmap;
return scoreInfo;
}
/// <summary>
/// Create a <see cref="ScoreInfo"/> from an API score instance.
/// </summary>
/// <param name="mods">The mod instances, resolved from a ruleset.</param>
/// <returns></returns>
public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo
{
OnlineID = OnlineID,
User = User ?? new APIUser { Id = UserID },
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
Ruleset = new RulesetInfo { OnlineID = RulesetID },
Passed = Passed,
TotalScore = TotalScore,
Accuracy = Accuracy,
MaxCombo = MaxCombo,
Rank = Rank,
Statistics = Statistics,
Date = EndedAt,
Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
Mods = mods,
PP = PP,
};
public long OnlineID => ID ?? -1;
}
}

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