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

Merge branch 'master' into pre-mod-multiplier-score

This commit is contained in:
Bartłomiej Dach 2024-05-08 13:39:44 +02:00
commit c9414da5d4
No known key found for this signature in database
192 changed files with 3895 additions and 1398 deletions

1
.gitignore vendored
View File

@ -341,3 +341,4 @@ inspectcode
FodyWeavers.xsd FodyWeavers.xsd
.idea/.idea.osu.Desktop/.idea/misc.xml .idea/.idea.osu.Desktop/.idea/misc.xml
.idea/.idea.osu.Android/.idea/deploymentTargetDropDown.xml

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.329.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.423.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -6,7 +6,6 @@ using System.Text;
using DiscordRPC; using DiscordRPC;
using DiscordRPC.Message; using DiscordRPC.Message;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
@ -80,14 +79,20 @@ namespace osu.Desktop
client.OnReady += onReady; client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
// A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate. try
// The library doesn't properly support URI registration when ran from an app bundle on macOS.
if (!RuntimeInfo.IsApple)
{ {
client.RegisterUriScheme(); client.RegisterUriScheme();
client.Subscribe(EventType.Join); client.Subscribe(EventType.Join);
client.OnJoin += onJoin; client.OnJoin += onJoin;
} }
catch (Exception ex)
{
// This is known to fail in at least the following sandboxed environments:
// - macOS (when packaged as an app bundle)
// - flatpak (see: https://github.com/flathub/sh.ppy.osu/issues/170)
// There is currently no better way to do this offered by Discord, so the best we can do is simply ignore it for now.
Logger.Log($"Failed to register Discord URI scheme: {ex}");
}
client.Initialize(); client.Initialize();
} }

View File

@ -489,6 +489,7 @@ namespace osu.Desktop
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16); public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvStatus internal enum NvStatus
{ {
OK = 0, // Success. Request is completed. OK = 0, // Success. Request is completed.
@ -611,6 +612,7 @@ namespace osu.Desktop
FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported. FIRMWARE_REVISION_NOT_SUPPORTED = -200, // The device's firmware is not supported.
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvSystemType internal enum NvSystemType
{ {
UNKNOWN = 0, UNKNOWN = 0,
@ -618,6 +620,7 @@ namespace osu.Desktop
DESKTOP = 2 DESKTOP = 2
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvGpuType internal enum NvGpuType
{ {
UNKNOWN = 0, UNKNOWN = 0,
@ -625,6 +628,7 @@ namespace osu.Desktop
DGPU = 2, // Discrete DGPU = 2, // Discrete
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvSettingID : uint internal enum NvSettingID : uint
{ {
OGL_AA_LINE_GAMMA_ID = 0x2089BF6C, OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
@ -717,6 +721,7 @@ namespace osu.Desktop
INVALID_SETTING_ID = 0xFFFFFFFF INVALID_SETTING_ID = 0xFFFFFFFF
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvShimSetting : uint internal enum NvShimSetting : uint
{ {
SHIM_RENDERING_MODE_INTEGRATED = 0x00000000, SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
@ -731,6 +736,7 @@ namespace osu.Desktop
SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvThreadControlSetting : uint internal enum NvThreadControlSetting : uint
{ {
OGL_THREAD_CONTROL_ENABLE = 0x00000001, OGL_THREAD_CONTROL_ENABLE = 0x00000001,

View File

@ -22,7 +22,7 @@ using osu.Game.IPC;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Performance; using osu.Game.Performance;
using osu.Game.Utils; using osu.Game.Utils;
using SDL2; using SDL;
namespace osu.Desktop namespace osu.Desktop
{ {
@ -161,7 +161,7 @@ namespace osu.Desktop
host.Window.Title = Name; host.Window.Title = Name;
} }
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo(); protected override BatteryInfo CreateBatteryInfo() => new SDL3BatteryInfo();
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
@ -170,13 +170,14 @@ namespace osu.Desktop
archiveImportIPCChannel?.Dispose(); archiveImportIPCChannel?.Dispose();
} }
private class SDL2BatteryInfo : BatteryInfo private unsafe class SDL3BatteryInfo : BatteryInfo
{ {
public override double? ChargeLevel public override double? ChargeLevel
{ {
get get
{ {
SDL.SDL_GetPowerInfo(out _, out int percentage); int percentage;
SDL3.SDL_GetPowerInfo(null, &percentage);
if (percentage == -1) if (percentage == -1)
return null; return null;
@ -185,7 +186,7 @@ namespace osu.Desktop
} }
} }
public override bool OnBattery => SDL.SDL_GetPowerInfo(out _, out _) == SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY; public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
} }
} }
} }

View File

@ -13,7 +13,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 SDL;
using Squirrel; using Squirrel;
namespace osu.Desktop namespace osu.Desktop
@ -51,18 +51,21 @@ namespace osu.Desktop
// While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher. // While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. 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/compatibility/ // See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)) if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
unsafe
{ {
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider // 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. // disabling it ourselves.
// We could also better detect compatibility mode if required: // 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 // 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, SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!", "Your operating system is too old to run osu!"u8,
"This version of osu! requires at least Windows 8.1 to run.\n" "This version of osu! requires at least Windows 8.1 to run.\n"u8
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n" + "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero); + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null);
return; return;
} }
}
setupSquirrel(); setupSquirrel();
} }

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.CodeAnalysis;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
@ -163,6 +164,7 @@ namespace osu.Desktop.Windows
[DllImport("Shell32.dll")] [DllImport("Shell32.dll")]
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2); private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
[SuppressMessage("ReSharper", "InconsistentNaming")]
private enum EventId private enum EventId
{ {
/// <summary> /// <summary>
@ -172,6 +174,7 @@ namespace osu.Desktop.Windows
SHCNE_ASSOCCHANGED = 0x08000000 SHCNE_ASSOCCHANGED = 0x08000000
} }
[SuppressMessage("ReSharper", "InconsistentNaming")]
private enum Flags : uint private enum Flags : uint
{ {
SHCNF_IDLIST = 0x0000 SHCNF_IDLIST = 0x0000

View File

@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Catch
: base(component) : base(component)
{ {
} }
protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
} }

View File

@ -15,10 +15,6 @@ namespace osu.Game.Rulesets.Mania
: base(component) : base(component)
{ {
} }
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
public enum ManiaSkinComponents public enum ManiaSkinComponents

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -52,6 +53,65 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
}
[Test]
public void TestDistanceSnapAdjustDoesNotHideTheGridIfStartingEnabled()
{
double distanceSnap = double.PositiveInfinity;
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value);
AddStep("increase distance", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.PressKey(Key.ControlLeft);
InputManager.ScrollVerticalBy(1);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.AltLeft);
});
AddUntilStep("distance snap increased", () => this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap));
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
}
[Test]
public void TestDistanceSnapAdjustShowsGridMomentarilyIfStartingDisabled()
{
double distanceSnap = double.PositiveInfinity;
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("store distance snap", () => distanceSnap = this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value);
AddStep("start increasing distance", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.PressKey(Key.ControlLeft);
});
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("finish increasing distance", () =>
{
InputManager.ScrollVerticalBy(1);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.AltLeft);
});
AddUntilStep("distance snap increased", () => this.ChildrenOfType<IDistanceSnapProvider>().First().DistanceSpacingMultiplier.Value, () => Is.GreaterThan(distanceSnap));
AddUntilStep("distance snap hidden in the end", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
} }
[Test] [Test]

View File

@ -30,23 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
}); });
[Test]
public void TestAddOverlappingControlPoints()
{
createVisualiser(true);
addControlPointStep(new Vector2(200));
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
AddAssert("last connection displayed", () =>
{
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300));
return lastConnection.DrawWidth > 50;
});
}
[Test] [Test]
public void TestPerfectCurveTooManyPoints() public void TestPerfectCurveTooManyPoints()
{ {
@ -194,24 +177,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addAssertPointPositionChanged(points, i); addAssertPointPositionChanged(points, i);
} }
[Test]
public void TestStackingUpdatesConnectionPosition()
{
createVisualiser(true);
Vector2 connectionPosition;
addControlPointStep(connectionPosition = new Vector2(300));
addControlPointStep(new Vector2(600));
// Apply a big number in stacking so the person running the test can clearly see if it fails
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10);
AddAssert($"Connection at {connectionPosition} changed",
() => visualiser.Connections[0].Position,
() => !Is.EqualTo(connectionPosition)
);
}
private void addAssertPointPositionChanged(Vector2[] points, int index) private void addAssertPointPositionChanged(Vector2[] points, int index)
{ {
AddAssert($"Point at {points.ElementAt(index)} changed", AddAssert($"Point at {points.ElementAt(index)} changed",

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -13,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Framework.Testing.Input; using osu.Framework.Testing.Input;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Skinning.Legacy;
@ -47,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
createTest(() => createTest(() =>
{ {
var skinContainer = new LegacySkinContainer(renderer, false); var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer); var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail; skinContainer.Child = legacyCursorTrail;
@ -61,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
createTest(() => createTest(() =>
{ {
var skinContainer = new LegacySkinContainer(renderer, true); var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer); var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail; skinContainer.Child = legacyCursorTrail;
@ -70,6 +72,22 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
} }
[Test]
public void TestLegacyDisjointCursorTrailViaNoCursor()
{
createTest(() =>
{
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: false, provideCursor: false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail;
return skinContainer;
});
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
}
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () => private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
{ {
Clear(); Clear();
@ -86,12 +104,14 @@ namespace osu.Game.Rulesets.Osu.Tests
private partial class LegacySkinContainer : Container, ISkinSource private partial class LegacySkinContainer : Container, ISkinSource
{ {
private readonly IRenderer renderer; private readonly IRenderer renderer;
private readonly bool disjoint; private readonly bool provideMiddle;
private readonly bool provideCursor;
public LegacySkinContainer(IRenderer renderer, bool disjoint) public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true)
{ {
this.renderer = renderer; this.renderer = renderer;
this.disjoint = disjoint; this.provideMiddle = provideMiddle;
this.provideCursor = provideCursor;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -102,15 +122,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
switch (componentName) switch (componentName)
{ {
case "cursortrail": case "cursor":
var tex = new Texture(renderer.WhitePixel); return provideCursor ? new Texture(renderer.WhitePixel) : null;
if (disjoint) case "cursortrail":
tex.ScaleAdjust = 1 / 25f; return new Texture(renderer.WhitePixel);
return tex;
case "cursormiddle": case "cursormiddle":
return disjoint ? null : renderer.WhitePixel; return provideMiddle ? null : renderer.WhitePixel;
} }
return null; return null;

View File

@ -4,10 +4,7 @@
#nullable disable #nullable disable
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osuTK; using osuTK;
@ -15,36 +12,21 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
/// <summary> /// <summary>
/// A visualisation of the line between two <see cref="PathControlPointPiece{T}"/>s. /// A visualisation of the lines between <see cref="PathControlPointPiece{T}"/>s.
/// </summary> /// </summary>
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnectionPiece{T}"/> visualises.</typeparam> /// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnection{T}"/> visualises.</typeparam>
public partial class PathControlPointConnectionPiece<T> : CompositeDrawable where T : OsuHitObject, IHasPath public partial class PathControlPointConnection<T> : SmoothPath where T : OsuHitObject, IHasPath
{ {
public readonly PathControlPoint ControlPoint;
private readonly Path path;
private readonly T hitObject; private readonly T hitObject;
public int ControlPointIndex { get; set; }
private IBindable<Vector2> hitObjectPosition; private IBindable<Vector2> hitObjectPosition;
private IBindable<int> pathVersion; private IBindable<int> pathVersion;
private IBindable<int> stackHeight; private IBindable<int> stackHeight;
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex) public PathControlPointConnection(T hitObject)
{ {
this.hitObject = hitObject; this.hitObject = hitObject;
ControlPointIndex = controlPointIndex; PathRadius = 1;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
ControlPoint = hitObject.Path.ControlPoints[controlPointIndex];
InternalChild = path = new SmoothPath
{
Anchor = Anchor.Centre,
PathRadius = 1
};
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -68,18 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
private void updateConnectingPath() private void updateConnectingPath()
{ {
Position = hitObject.StackedPosition + ControlPoint.Position; Position = hitObject.StackedPosition;
path.ClearVertices(); ClearVertices();
int nextIndex = ControlPointIndex + 1; foreach (var controlPoint in hitObject.Path.ControlPoints)
if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count) AddVertex(controlPoint.Position);
return;
path.AddVertex(Vector2.Zero); OriginPosition = PositionInBoundingBox(Vector2.Zero);
path.AddVertex(hitObject.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
} }
} }
} }

View File

@ -37,7 +37,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
internal readonly Container<PathControlPointPiece<T>> Pieces; internal readonly Container<PathControlPointPiece<T>> Pieces;
internal readonly Container<PathControlPointConnectionPiece<T>> Connections;
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>(); private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly T hitObject; private readonly T hitObject;
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
Connections = new Container<PathControlPointConnectionPiece<T>> { RelativeSizeAxes = Axes.Both }, new PathControlPointConnection<T>(hitObject),
Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both } Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both }
}; };
} }
@ -78,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPoints.BindTo(hitObject.Path.ControlPoints); controlPoints.BindTo(hitObject.Path.ControlPoints);
} }
// Generally all the control points are within the visible area all the time.
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => true;
/// <summary> /// <summary>
/// Handles correction of invalid path types. /// Handles correction of invalid path types.
/// </summary> /// </summary>
@ -185,17 +187,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
Debug.Assert(e.NewItems != null); Debug.Assert(e.NewItems != null);
// If inserting in the path (not appending),
// update indices of existing connections after insert location
if (e.NewStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.NewStartingIndex)
connection.ControlPointIndex += e.NewItems.Count;
}
}
for (int i = 0; i < e.NewItems.Count; i++) for (int i = 0; i < e.NewItems.Count; i++)
{ {
var point = (PathControlPoint)e.NewItems[i]; var point = (PathControlPoint)e.NewItems[i];
@ -209,8 +200,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
d.DragInProgress = DragInProgress; d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded; d.DragEnded = DragEnded;
})); }));
Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
} }
break; break;
@ -222,19 +211,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray()) foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
piece.RemoveAndDisposeImmediately(); piece.RemoveAndDisposeImmediately();
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
connection.RemoveAndDisposeImmediately();
}
// If removing before the end of the path,
// update indices of connections after remove location
if (e.OldStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.OldStartingIndex)
connection.ControlPointIndex -= e.OldItems.Count;
}
} }
break; break;

View File

@ -403,7 +403,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override MenuItem[] ContextMenuItems => new MenuItem[] public override MenuItem[] ContextMenuItems => new MenuItem[]
{ {
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
{
changeHandler?.BeginChange();
addControlPoint(rightClickPosition);
changeHandler?.EndChange();
}),
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream), new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
}; };

View File

@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Osu
: base(component) : base(component)
{ {
} }
protected override string RulesetPrefix => OsuRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
} }

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly ISkin skin; private readonly ISkin skin;
private const double disjoint_trail_time_separation = 1000 / 60.0; private const double disjoint_trail_time_separation = 1000 / 60.0;
private bool disjointTrail; public bool DisjointTrail { get; private set; }
private double lastTrailTime; private double lastTrailTime;
private IBindable<float> cursorSize = null!; private IBindable<float> cursorSize = null!;
@ -31,14 +31,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config, ISkinSource skinSource)
{ {
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy(); cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
Texture = skin.GetTexture("cursortrail"); Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
if (disjointTrail) // Cursor and cursor trail components are sourced from potentially different skin sources.
// Stable always chooses cursor trail disjoint behaviour based on the cursor texture lookup source, so we need to fetch where that occurred.
// See https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Skinning/SkinManager.cs#L269
var cursorProvider = skinSource.FindProvider(s => s.GetTexture("cursor") != null);
DisjointTrail = cursorProvider?.GetTexture("cursormiddle") == null;
if (DisjointTrail)
{ {
bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true; bool centre = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorCentre)?.Value ?? true;
@ -57,19 +62,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
} }
protected override double FadeDuration => disjointTrail ? 150 : 500; protected override double FadeDuration => DisjointTrail ? 150 : 500;
protected override float FadeExponent => 1; protected override float FadeExponent => 1;
protected override bool InterpolateMovements => !disjointTrail; protected override bool InterpolateMovements => !DisjointTrail;
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
protected override bool AvoidDrawingNearCursor => !disjointTrail; protected override bool AvoidDrawingNearCursor => !DisjointTrail;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (!disjointTrail || !currentPosition.HasValue) if (!DisjointTrail || !currentPosition.HasValue)
return; return;
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation) if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
if (!disjointTrail) if (!DisjointTrail)
return base.OnMouseMove(e); return base.OnMouseMove(e);
currentPosition = e.ScreenSpaceMousePosition; currentPosition = e.ScreenSpaceMousePosition;

View File

@ -11,9 +11,5 @@ namespace osu.Game.Rulesets.Taiko
: base(component) : base(component)
{ {
} }
protected override string RulesetPrefix => TaikoRuleset.SHORT_NAME;
protected override string ComponentName => Component.ToString().ToLowerInvariant();
} }
} }

View File

@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
@ -37,6 +38,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal)); private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
[Test]
public void TestUnsupportedStoryboardEvents()
{
const string name = "Resources/storyboard_only_video.osu";
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
Assert.That(decoded.beatmap.UnhandledEventLines.Count, Is.EqualTo(1));
Assert.That(decoded.beatmap.UnhandledEventLines.Single(), Is.EqualTo("Video,0,\"video.avi\""));
var memoryStream = encodeToLegacy(decoded);
var storyboard = new LegacyStoryboardDecoder().Decode(new LineBufferedReader(memoryStream));
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(video.Elements.Count, Is.EqualTo(1));
}
[TestCaseSource(nameof(allBeatmaps))] [TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name) public void TestEncodeDecodeStability(string name)
{ {

View File

@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.IO.Legacy; using osu.Game.IO.Legacy;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
@ -31,6 +32,7 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy; using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Users;
namespace osu.Game.Tests.Beatmaps.Formats namespace osu.Game.Tests.Beatmaps.Formats
{ {
@ -224,6 +226,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
}; };
scoreInfo.OnlineID = 123123; scoreInfo.OnlineID = 123123;
scoreInfo.User = new APIUser
{
Username = "spaceman_atlas",
Id = 3035836,
CountryCode = CountryCode.PL
};
scoreInfo.ClientVersion = "2023.1221.0"; scoreInfo.ClientVersion = "2023.1221.0";
var beatmap = new TestBeatmap(ruleset); var beatmap = new TestBeatmap(ruleset);
@ -248,6 +256,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics)); Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods)); Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0")); Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
Assert.That(decodedAfterEncode.ScoreInfo.RealmUser.OnlineID, Is.EqualTo(3035836));
}); });
} }
@ -352,6 +361,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[HitResult.Great] = 200, [HitResult.Great] = 200,
[HitResult.LargeTickHit] = 1, [HitResult.LargeTickHit] = 1,
}; };
scoreInfo.Rank = ScoreRank.A;
var beatmap = new TestBeatmap(ruleset); var beatmap = new TestBeatmap(ruleset);
var score = new Score var score = new Score

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestCase(1, 3)] [TestCase(1, 3)]
[TestCase(1, 0)] [TestCase(1, 0)]
[TestCase(0, 3)] [TestCase(0, 3)]
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount) public void TestCatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
{ {
var ruleset = new CatchRuleset().RulesetInfo; var ruleset = new CatchRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
@ -41,7 +41,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
[Test] [Test]
public void ScoreWithMissIsNotPerfect() public void TestFailPreserved()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo();
var beatmap = new TestBeatmap(ruleset);
scoreInfo.Rank = ScoreRank.F;
var score = new Score { ScoreInfo = scoreInfo };
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.F));
}
[Test]
public void TestScoreWithMissIsNotPerfect()
{ {
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset); var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);

View File

@ -0,0 +1,128 @@
// 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.IO;
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK.Audio;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckHitsoundsFormatTest
{
private CheckHitsoundsFormat check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckHitsoundsFormat();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile("wav") }
}
}
};
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
}
[Test]
public void TestMp3Audio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateIncorrectFormat);
}
}
[Test]
public void TestOggAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestWavAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestWebmAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported);
}
}
[Test]
public void TestNotAnAudioFile()
{
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile("png") }
}
}
};
using (var resourceStream = TestResources.OpenResource("Textures/test-image.png"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestCorruptAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckHitsoundsFormat.IssueTemplateFormatUnsupported);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -0,0 +1,112 @@
// 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.IO;
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK.Audio;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public partial class CheckSongFormatTest
{
private CheckSongFormat check = null!;
private IBeatmap beatmap = null!;
[SetUp]
public void Setup()
{
check = new CheckSongFormat();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile("mp3") }
}
}
};
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
}
[Test]
public void TestMp3Audio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestOggAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.ogg"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(0));
}
}
[Test]
public void TestWavAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/hitsound-delay.wav"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateIncorrectFormat);
}
}
[Test]
public void TestWebmAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.webm"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported);
}
}
[Test]
public void TestCorruptAudio()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
beatmap.Metadata.AudioFile = "abc123.mp3";
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckSongFormat.IssueTemplateFormatUnsupported);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -95,18 +95,6 @@ namespace osu.Game.Tests.Editing.Checks
} }
} }
[Test]
public void TestCorruptAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream) private BeatmapVerifierContext getContext(Stream? resourceStream)
{ {
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null); var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);

View File

@ -0,0 +1,235 @@
// 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 System.Threading;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class TestSceneTimedDifficultyCalculation
{
[Test]
public void TestAttributesGeneratedForAllNonSkippedObjects()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
new TestHitObject { StartTime = 1 },
new TestHitObject
{
StartTime = 2,
Nested = 1
},
new TestHitObject { StartTime = 3 },
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(4));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object.
assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestAttributesNotGeneratedForSkippedObjects()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
// The first object is usually skipped in all implementations
new TestHitObject
{
StartTime = 1,
Skip = true
},
// An intermediate skipped object.
new TestHitObject
{
StartTime = 2,
Skip = true
},
new TestHitObject { StartTime = 3 },
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(1));
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestNestedObjectOnlyAddsParentOnce()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
new TestHitObject
{
StartTime = 1,
Skip = true,
Nested = 2
},
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(2));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0]);
}
[Test]
public void TestSkippedLastObjectAddedInLastIteration()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
new TestHitObject { StartTime = 1 },
new TestHitObject
{
StartTime = 2,
Skip = true
},
new TestHitObject
{
StartTime = 3,
Skip = true
},
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(1));
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected)
{
Assert.That(((TestDifficultyAttributes)attribs.Attributes).Objects, Is.EquivalentTo(expected));
}
private class TestHitObject : HitObject
{
/// <summary>
/// Whether to skip generating a difficulty representation for this object.
/// </summary>
public bool Skip { get; set; }
/// <summary>
/// Whether to generate nested difficulty representations for this object, and if so, how many.
/// </summary>
public int Nested { get; set; }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
for (int i = 0; i < Nested; i++)
AddNested(new TestHitObject { StartTime = StartTime + 0.1 * i });
}
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => Enumerable.Empty<Mod>();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => throw new NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new PassThroughBeatmapConverter(beatmap);
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TestDifficultyCalculator(beatmap);
public override string Description => string.Empty;
public override string ShortName => string.Empty;
private class PassThroughBeatmapConverter : IBeatmapConverter
{
public event Action<HitObject, IEnumerable<HitObject>>? ObjectConverted
{
add { }
remove { }
}
public IBeatmap Beatmap { get; }
public PassThroughBeatmapConverter(IBeatmap beatmap)
{
Beatmap = beatmap;
}
public bool CanConvert() => true;
public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap;
}
}
private class TestDifficultyCalculator : DifficultyCalculator
{
public TestDifficultyCalculator(IWorkingBeatmap beatmap)
: base(new TestRuleset().RulesetInfo, beatmap)
{
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
=> new TestDifficultyAttributes { Objects = beatmap.HitObjects.ToArray() };
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
foreach (var obj in beatmap.HitObjects.OfType<TestHitObject>())
{
if (!obj.Skip)
objects.Add(new DifficultyHitObject(obj, obj, clockRate, objects, objects.Count));
foreach (var nested in obj.NestedHitObjects)
objects.Add(new DifficultyHitObject(nested, nested, clockRate, objects, objects.Count));
}
return objects;
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { new PassThroughSkill(mods) };
private class PassThroughSkill : Skill
{
public PassThroughSkill(Mod[] mods)
: base(mods)
{
}
public override void Process(DifficultyHitObject current)
{
}
public override double DifficultyValue() => 1;
}
}
private class TestDifficultyAttributes : DifficultyAttributes
{
public HitObject[] Objects = Array.Empty<HitObject>();
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -15,6 +15,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -23,6 +24,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Users;
namespace osu.Game.Tests.Scores.IO namespace osu.Game.Tests.Scores.IO
{ {
@ -284,6 +286,272 @@ namespace osu.Game.Tests.Scores.IO
} }
} }
[Test]
public void TestUserLookedUpByUsernameForOnlineScoreIfUserIDMissing()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
CountryCode = CountryCode.JP,
Id = 1234
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineID = 12345,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
Assert.AreEqual(1234, imported.RealmUser.OnlineID);
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestUserLookedUpByUsernameForLegacyOnlineScore()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
CountryCode = CountryCode.JP,
Id = 1234
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
LegacyOnlineID = 12345,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
Assert.AreEqual(1234, imported.RealmUser.OnlineID);
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestUserNotLookedUpForOfflineScoreIfUserIDMissing()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
CountryCode = CountryCode.JP,
Id = 1234
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineID = -1,
LegacyOnlineID = -1,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
Assert.That(imported.RealmUser.OnlineID, Is.LessThanOrEqualTo(1));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestUserLookedUpByOnlineIDIfPresent([Values] bool isOnlineScore)
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "5555")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Some other guy",
CountryCode = CountryCode.DE,
Id = 5555
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Id = 5555 },
Date = DateTimeOffset.Now,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
if (isOnlineScore)
toImport.OnlineID = 12345;
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual("Some other guy", imported.RealmUser.Username);
Assert.AreEqual(5555, imported.RealmUser.OnlineID);
Assert.AreEqual(CountryCode.DE, imported.RealmUser.CountryCode);
}
finally
{
host.Exit();
}
}
}
public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{ {
// clone to avoid attaching the input score to realm. // clone to avoid attaching the input score to realm.

View File

@ -1,6 +1,7 @@
// 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.IO;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using MemoryStream = System.IO.MemoryStream;
namespace osu.Game.Tests.Skins namespace osu.Game.Tests.Skins
{ {
@ -21,6 +23,52 @@ namespace osu.Game.Tests.Skins
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } = null!; private BeatmapManager beatmaps { get; set; } = null!;
[Test]
public void TestRetrieveAndLegacyExportJapaneseFilename()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz"));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
// Ensure exporter encoding is correct (round trip)
AddStep("export", () =>
{
outStream = new MemoryStream();
new LegacyBeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
}
[Test]
public void TestRetrieveAndNonLegacyExportJapaneseFilename()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"japanese-filename.osz"));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
// Ensure exporter encoding is correct (round trip)
AddStep("export", () =>
{
outStream = new MemoryStream();
new BeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"見本")) != null);
}
[Test] [Test]
public void TestRetrieveOggAudio() public void TestRetrieveOggAudio()
{ {
@ -45,6 +93,12 @@ namespace osu.Game.Tests.Skins
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null); AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
} }
private IWorkingBeatmap importBeatmapFromStream(Stream stream)
{
var imported = beatmaps.Import(new ImportTask(stream, "filename.osz")).GetResultSafely();
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
}
private IWorkingBeatmap importBeatmapFromArchives(string filename) private IWorkingBeatmap importBeatmapFromArchives(string filename)
{ {
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely(); var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input; using osuTK.Input;
@ -83,6 +84,49 @@ namespace osu.Game.Tests.Visual.Editing
} }
} }
[Test]
public void TestDeleteDifficultyWithPendingChanges()
{
Guid deletedDifficultyID = Guid.Empty;
int countBeforeDeletion = 0;
string beatmapSetHashBefore = string.Empty;
AddUntilStep("wait for editor to load", () => Editor?.ReadyForUse == true);
AddStep("store selected difficulty", () =>
{
deletedDifficultyID = EditorBeatmap.BeatmapInfo.ID;
countBeforeDeletion = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count;
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
});
AddStep("make change to difficulty", () =>
{
EditorBeatmap.BeginChange();
EditorBeatmap.BeatmapInfo.DifficultyName = "changin' things";
EditorBeatmap.EndChange();
});
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
AddAssert("dialog is deletion confirmation dialog", () => DialogOverlay.CurrentDialog, Is.InstanceOf<DeleteDifficultyConfirmationDialog>);
AddStep("confirm", () => InputManager.Key(Key.Number1));
AddUntilStep("no next dialog", () => DialogOverlay.CurrentDialog == null);
AddUntilStep("switched to different difficulty",
() => this.ChildrenOfType<EditorBeatmap>().SingleOrDefault() != null && EditorBeatmap.BeatmapInfo.ID != deletedDifficultyID);
AddAssert("difficulty is unattached from set",
() => Beatmap.Value.BeatmapSetInfo.Beatmaps.Select(b => b.ID), () => Does.Not.Contain(deletedDifficultyID));
AddAssert("beatmap set difficulty count decreased by one",
() => Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, () => Is.EqualTo(countBeforeDeletion - 1));
AddAssert("set hash changed", () => Beatmap.Value.BeatmapSetInfo.Hash, () => Is.Not.EqualTo(beatmapSetHashBefore));
AddAssert("difficulty is deleted from realm",
() => Realm.Run(r => r.Find<BeatmapInfo>(deletedDifficultyID)), () => Is.Null);
}
private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType<DrawableOsuMenuItem>() private DrawableOsuMenuItem getDeleteMenuItem() => this.ChildrenOfType<DrawableOsuMenuItem>()
.Single(item => item.ChildrenOfType<SpriteText>().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal))); .Single(item => item.ChildrenOfType<SpriteText>().Any(text => text.Text.ToString().StartsWith("Delete", StringComparison.Ordinal)));
} }

View File

@ -10,6 +10,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("disallow all lookups", () => AddStep("disallow all lookups", () =>
{ {
storyboard.UseSkinSprites = false; storyboard.UseSkinSprites = false;
storyboard.AlwaysProvideTexture = false; storyboard.ProvideResources = false;
}); });
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow storyboard lookup", () => AddStep("allow storyboard lookup", () =>
{ {
storyboard.UseSkinSprites = false; storyboard.UseSkinSprites = false;
storyboard.AlwaysProvideTexture = true; storyboard.ProvideResources = true;
}); });
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -67,13 +68,48 @@ namespace osu.Game.Tests.Visual.Gameplay
assertStoryboardSourced(); assertStoryboardSourced();
} }
[TestCase(false)]
[TestCase(true)]
public void TestVideo(bool scaleTransformProvided)
{
AddStep("allow storyboard lookup", () =>
{
storyboard.ProvideResources = true;
});
AddStep("create video", () => SetContents(_ =>
{
var layer = storyboard.GetLayer("Video");
var sprite = new StoryboardVideo("Videos/test-video.mp4", Time.Current);
if (scaleTransformProvided)
{
sprite.Commands.AddScale(Easing.None, Time.Current, Time.Current + 1000, 1, 2);
sprite.Commands.AddScale(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1);
}
layer.Elements.Clear();
layer.Add(sprite);
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
storyboard.CreateDrawable()
}
};
}));
}
[Test] [Test]
public void TestSkinLookupPreferredOverStoryboard() public void TestSkinLookupPreferredOverStoryboard()
{ {
AddStep("allow all lookups", () => AddStep("allow all lookups", () =>
{ {
storyboard.UseSkinSprites = true; storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true; storyboard.ProvideResources = true;
}); });
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -91,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow skin lookup", () => AddStep("allow skin lookup", () =>
{ {
storyboard.UseSkinSprites = true; storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = false; storyboard.ProvideResources = false;
}); });
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -109,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow all lookups", () => AddStep("allow all lookups", () =>
{ {
storyboard.UseSkinSprites = true; storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true; storyboard.ProvideResources = true;
}); });
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -127,7 +163,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow all lookups", () => AddStep("allow all lookups", () =>
{ {
storyboard.UseSkinSprites = true; storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true; storyboard.ProvideResources = true;
}); });
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -142,7 +178,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow all lookups", () => AddStep("allow all lookups", () =>
{ {
storyboard.UseSkinSprites = true; storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true; storyboard.ProvideResources = true;
}); });
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -156,7 +192,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow all lookups", () => AddStep("allow all lookups", () =>
{ {
storyboard.UseSkinSprites = true; storyboard.UseSkinSprites = true;
storyboard.AlwaysProvideTexture = true; storyboard.ProvideResources = true;
}); });
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
@ -170,17 +206,25 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft)); AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft));
} }
private DrawableStoryboard createSprite(string lookupName, Anchor origin, Vector2 initialPosition) private Drawable createSprite(string lookupName, Anchor origin, Vector2 initialPosition)
{ {
var layer = storyboard.GetLayer("Background"); var layer = storyboard.GetLayer("Background");
var sprite = new StoryboardSprite(lookupName, origin, initialPosition); var sprite = new StoryboardSprite(lookupName, origin, initialPosition);
sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1); var loop = sprite.AddLoopingGroup(Time.Current, 100);
loop.AddAlpha(Easing.None, 0, 10000, 1, 1);
layer.Elements.Clear(); layer.Elements.Clear();
layer.Add(sprite); layer.Add(sprite);
return storyboard.CreateDrawable().With(s => s.RelativeSizeAxes = Axes.Both); return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
storyboard.CreateDrawable()
}
};
} }
private void assertStoryboardSourced() private void assertStoryboardSourced()
@ -202,42 +246,52 @@ namespace osu.Game.Tests.Visual.Gameplay
return new TestDrawableStoryboard(this, mods); return new TestDrawableStoryboard(this, mods);
} }
public bool AlwaysProvideTexture { get; set; } public bool ProvideResources { get; set; }
public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty; public override string GetStoragePathFromStoryboardPath(string path) => ProvideResources ? path : string.Empty;
private partial class TestDrawableStoryboard : DrawableStoryboard private partial class TestDrawableStoryboard : DrawableStoryboard
{ {
private readonly bool alwaysProvideTexture; private readonly bool provideResources;
public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList<Mod>? mods) public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList<Mod>? mods)
: base(storyboard, mods) : base(storyboard, mods)
{ {
alwaysProvideTexture = storyboard.AlwaysProvideTexture; provideResources = storyboard.ProvideResources;
} }
protected override IResourceStore<byte[]> CreateResourceLookupStore() => alwaysProvideTexture protected override IResourceStore<byte[]> CreateResourceLookupStore() => provideResources
? new AlwaysReturnsTextureStore() ? new ResourcesTextureStore()
: new ResourceStore<byte[]>(); : new ResourceStore<byte[]>();
internal class AlwaysReturnsTextureStore : IResourceStore<byte[]> internal class ResourcesTextureStore : IResourceStore<byte[]>
{ {
private const string test_image = "Resources/Textures/test-image.png";
private readonly DllResourceStore store; private readonly DllResourceStore store;
public AlwaysReturnsTextureStore() public ResourcesTextureStore()
{ {
store = TestResources.GetStore(); store = TestResources.GetStore();
} }
public void Dispose() => store.Dispose(); public void Dispose() => store.Dispose();
public byte[] Get(string name) => store.Get(test_image); public byte[] Get(string name) => store.Get(map(name));
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken); public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(map(name), cancellationToken);
public Stream GetStream(string name) => store.GetStream(test_image); public Stream GetStream(string name) => store.GetStream(map(name));
private string map(string name)
{
switch (name)
{
case lookup_name:
return "Resources/Textures/test-image.png";
default:
return $"Resources/{name}";
}
}
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources(); public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
} }

View File

@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); sprite.Commands.AddAlpha(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
storyboard.GetLayer("Background").Add(sprite); storyboard.GetLayer("Background").Add(sprite);
@ -73,17 +73,17 @@ namespace osu.Game.Tests.Visual.Gameplay
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
// these should be ignored as we have an alpha visibility blocker proceeding this command. // these should be ignored as we have an alpha visibility blocker proceeding this command.
sprite.TimelineGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1); sprite.Commands.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
var loopGroup = sprite.AddLoop(loop_start_time, 50); var loopGroup = sprite.AddLoopingGroup(loop_start_time, 50);
loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1); loopGroup.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
var target = addEventToLoop ? loopGroup : sprite.TimelineGroup; var target = addEventToLoop ? loopGroup : sprite.Commands;
double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0; double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0;
target.Alpha.Add(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1); target.AddAlpha(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1);
// these should be ignored due to being in the future. // these should be ignored due to being in the future.
sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); sprite.Commands.AddAlpha(Easing.None, 18000, 20000, 0, 1);
loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1); loopGroup.AddAlpha(Easing.None, 38000, 40000, 0, 1);
storyboard.GetLayer("Background").Add(sprite); storyboard.GetLayer("Background").Add(sprite);

View File

@ -0,0 +1,264 @@
// 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneStoryboardCommands : OsuTestScene
{
[Cached(typeof(Storyboard))]
private TestStoryboard storyboard { get; set; } = new TestStoryboard
{
UseSkinSprites = false,
AlwaysProvideTexture = true,
};
private readonly ManualClock manualClock = new ManualClock { Rate = 1, IsRunning = true };
private int clockDirection;
private const string lookup_name = "hitcircleoverlay";
private const double clock_limit = 2500;
protected override Container<Drawable> Content => content;
private Container content = null!;
private SpriteText timelineText = null!;
private Box timelineMarker = null!;
[BackgroundDependencyLoader]
private void load()
{
base.Content.Children = new Drawable[]
{
content = new Container
{
RelativeSizeAxes = Axes.Both,
},
timelineText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Bottom = 60 },
},
timelineMarker = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomCentre,
RelativePositionAxes = Axes.X,
Size = new Vector2(2, 50),
},
};
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("start clock", () => clockDirection = 1);
AddStep("pause clock", () => clockDirection = 0);
AddStep("set clock = 0", () => manualClock.CurrentTime = 0);
}
[Test]
public void TestNormalCommandPlayback()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
s.Commands.AddY(Easing.OutBounce, 500, 900, 100, 240);
s.Commands.AddY(Easing.OutQuint, 1100, 1500, 240, 100);
}));
assert(0, 100);
assert(500, 100);
assert(1000, 240);
assert(1500, 100);
assert(clock_limit, 100);
assert(1500, 100);
assert(1000, 240);
assert(500, 100);
assert(0, 100);
void assert(double time, double y)
{
AddStep($"set clock = {time}", () => manualClock.CurrentTime = time);
AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().Y == y);
}
}
[Test]
public void TestLoopingCommandsPlayback()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
var loop = s.AddLoopingGroup(250, 1);
loop.AddY(Easing.OutBounce, 0, 400, 100, 240);
loop.AddY(Easing.OutQuint, 600, 1000, 240, 100);
}));
assert(0, 100);
assert(250, 100);
assert(850, 240);
assert(1250, 100);
assert(1850, 240);
assert(2250, 100);
assert(clock_limit, 100);
assert(2250, 100);
assert(1850, 240);
assert(1250, 100);
assert(850, 240);
assert(250, 100);
assert(0, 100);
void assert(double time, double y)
{
AddStep($"set clock = {time}", () => manualClock.CurrentTime = time);
AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().Y == y);
}
}
[Test]
public void TestLoopManyTimes()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
var loop = s.AddLoopingGroup(500, 10000);
loop.AddY(Easing.OutBounce, 0, 60, 100, 240);
loop.AddY(Easing.OutQuint, 80, 120, 240, 100);
}));
}
[Test]
public void TestParameterTemporaryEffect()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
s.Commands.AddFlipV(Easing.None, 1000, 1500, true, false);
}));
AddAssert("sprite not flipped at t = 0", () => !this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250);
AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000);
AddAssert("sprite not flipped at t = 2000", () => !this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("resume clock", () => clockDirection = 1);
}
[Test]
public void TestParameterPermanentEffect()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
s.Commands.AddFlipV(Easing.None, 1000, 1000, true, true);
}));
AddAssert("sprite flipped at t = 0", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250);
AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000);
AddAssert("sprite flipped at t = 2000", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("resume clock", () => clockDirection = 1);
}
protected override void Update()
{
base.Update();
if (manualClock.CurrentTime > clock_limit || manualClock.CurrentTime < 0)
clockDirection = -clockDirection;
manualClock.CurrentTime += Time.Elapsed * clockDirection;
timelineText.Text = $"Time: {manualClock.CurrentTime:0}ms";
timelineMarker.X = (float)(manualClock.CurrentTime / clock_limit);
}
private DrawableStoryboard createStoryboard(Action<StoryboardSprite>? addCommands = null)
{
var layer = storyboard.GetLayer("Background");
var sprite = new StoryboardSprite(lookup_name, Anchor.Centre, new Vector2(320, 240));
sprite.Commands.AddScale(Easing.None, 0, clock_limit, 0.5f, 0.5f);
sprite.Commands.AddAlpha(Easing.None, 0, clock_limit, 1, 1);
addCommands?.Invoke(sprite);
layer.Elements.Clear();
layer.Add(sprite);
return storyboard.CreateDrawable().With(c => c.Clock = new FramedClock(manualClock));
}
private partial class TestStoryboard : Storyboard
{
public override DrawableStoryboard CreateDrawable(IReadOnlyList<Mod>? mods = null)
{
return new TestDrawableStoryboard(this, mods);
}
public bool AlwaysProvideTexture { get; set; }
public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty;
private partial class TestDrawableStoryboard : DrawableStoryboard
{
private readonly bool alwaysProvideTexture;
public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList<Mod>? mods)
: base(storyboard, mods)
{
alwaysProvideTexture = storyboard.AlwaysProvideTexture;
}
protected override IResourceStore<byte[]> CreateResourceLookupStore() => alwaysProvideTexture
? new AlwaysReturnsTextureStore()
: new ResourceStore<byte[]>();
internal class AlwaysReturnsTextureStore : IResourceStore<byte[]>
{
private const string test_image = "Resources/Textures/test-image.png";
private readonly DllResourceStore store;
public AlwaysReturnsTextureStore()
{
store = TestResources.GetStore();
}
public void Dispose() => store.Dispose();
public byte[] Get(string name) => store.Get(test_image);
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken);
public Stream GetStream(string name) => store.GetStream(test_image);
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
}
}
}
}
}

View File

@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
var storyboard = new Storyboard(); var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, startTime, 0, 0, 1); sprite.Commands.AddAlpha(Easing.None, startTime, 0, 0, 1);
storyboard.GetLayer("Background").Add(sprite); storyboard.GetLayer("Background").Add(sprite);
return storyboard; return storyboard;
} }

View File

@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
var storyboard = new Storyboard(); var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0); sprite.Commands.AddAlpha(Easing.None, 0, duration, 1, 0);
storyboard.GetLayer("Background").Add(sprite); storyboard.GetLayer("Background").Add(sprite);
return storyboard; return storyboard;
} }

View File

@ -69,8 +69,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}), }),
createLoungeRoom(new Room createLoungeRoom(new Room
{ {
Name = { Value = "Multiplayer room" }, Name = { Value = "Private room" },
Status = { Value = new RoomStatusOpen() }, Status = { Value = new RoomStatusOpenPrivate() },
HasPassword = { Value = true },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
Type = { Value = MatchType.HeadToHead }, Type = { Value = MatchType.HeadToHead },
Playlist = Playlist =

View File

@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestIntroStoryboardElement() => testLeadIn(b => public void TestIntroStoryboardElement() => testLeadIn(b =>
{ {
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, -2000, 0, 0, 1); sprite.Commands.AddAlpha(Easing.None, -2000, 0, 0, 1);
b.Storyboard.GetLayer("Background").Add(sprite); b.Storyboard.GetLayer("Background").Add(sprite);
}); });

View File

@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Navigation
BeatmapInfo = beatmap.Beatmaps.First(), BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, Ruleset = ruleset ?? new OsuRuleset().RulesetInfo,
User = new GuestUser(), User = new GuestUser(),
}).Value; })!.Value;
}); });
AddAssert($"import {i} succeeded", () => imported != null); AddAssert($"import {i} succeeded", () => imported != null);

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -321,6 +322,30 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().All(manager => !manager.UseParentInput)); AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().All(manager => !manager.UseParentInput));
} }
[Test]
public void TestSkinSavesOnChange()
{
advanceToSongSelect();
openSkinEditor();
Guid editedSkinId = Guid.Empty;
AddStep("save skin id", () => editedSkinId = Game.Dependencies.Get<SkinManager>().CurrentSkinInfo.Value.ID);
AddStep("add skinnable component", () =>
{
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick();
});
AddStep("change to triangles skin", () => Game.Dependencies.Get<SkinManager>().SetSkinFromConfiguration(SkinInfo.TRIANGLES_SKIN.ToString()));
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
// sort of implicitly relies on song select not being skinnable.
// TODO: revisit if the above ever changes
AddUntilStep("skin changed", () => !skinEditor.ChildrenOfType<SkinBlueprint>().Any());
AddStep("change back to modified skin", () => Game.Dependencies.Get<SkinManager>().SetSkinFromConfiguration(editedSkinId.ToString()));
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
AddUntilStep("changes saved", () => skinEditor.ChildrenOfType<SkinBlueprint>().Any());
}
private void advanceToSongSelect() private void advanceToSongSelect()
{ {
PushAndConfirm(() => songSelect = new TestPlaySongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());

View File

@ -82,6 +82,14 @@ namespace osu.Game.Tests.Visual.Ranking
}).ToList()); }).ToList());
} }
[Test]
public void TestNonBasicHitResultsAreIgnored()
{
createTest(CreateDistributedHitEvents(0, 50)
.Select(h => new HitEvent(h.TimeOffset, 1.0, h.TimeOffset > 0 ? HitResult.Ok : HitResult.LargeTickHit, placeholder_object, placeholder_object, null))
.ToList());
}
[Test] [Test]
public void TestMultipleWindowsOfHitResult() public void TestMultipleWindowsOfHitResult()
{ {

View File

@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Ranking
scores.Add(score); scores.Add(score);
} }
scoresCallback?.Invoke(scores); scoresCallback.Invoke(scores);
return null; return null;
} }

View File

@ -296,7 +296,7 @@ namespace osu.Game.Tests.Visual.Settings
} }
[Test] [Test]
public void TestBindingConflictResolvedByRollback() public void TestBindingConflictResolvedByRollbackViaMouse()
{ {
AddStep("reset taiko section to default", () => AddStep("reset taiko section to default", () =>
{ {
@ -315,7 +315,7 @@ namespace osu.Game.Tests.Visual.Settings
} }
[Test] [Test]
public void TestBindingConflictResolvedByOverwrite() public void TestBindingConflictResolvedByOverwriteViaMouse()
{ {
AddStep("reset taiko section to default", () => AddStep("reset taiko section to default", () =>
{ {
@ -333,6 +333,46 @@ namespace osu.Game.Tests.Visual.Settings
checkBinding("Left (rim)", "M1"); checkBinding("Left (rim)", "M1");
} }
[Test]
public void TestBindingConflictResolvedByRollbackViaKeyboard()
{
AddStep("reset taiko & global sections to default", () =>
{
panel.ChildrenOfType<VariantBindingsSubsection>().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset))
.ChildrenOfType<ResetButton>().Single().TriggerClick();
panel.ChildrenOfType<ResetButton>().First().TriggerClick();
});
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
scrollToAndStartBinding("Left (rim)");
AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for popover", () => panel.ChildrenOfType<KeyBindingConflictPopover>().SingleOrDefault(), () => Is.Not.Null);
AddStep("press Esc", () => InputManager.Key(Key.Escape));
checkBinding("Left (centre)", "M1");
checkBinding("Left (rim)", "M2");
}
[Test]
public void TestBindingConflictResolvedByOverwriteViaKeyboard()
{
AddStep("reset taiko & global sections to default", () =>
{
panel.ChildrenOfType<VariantBindingsSubsection>().First(section => new TaikoRuleset().RulesetInfo.Equals(section.Ruleset))
.ChildrenOfType<ResetButton>().Single().TriggerClick();
panel.ChildrenOfType<ResetButton>().First().TriggerClick();
});
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre));
scrollToAndStartBinding("Left (rim)");
AddStep("attempt to bind M1 to two keys", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for popover", () => panel.ChildrenOfType<KeyBindingConflictPopover>().SingleOrDefault(), () => Is.Not.Null);
AddStep("press Enter", () => InputManager.Key(Key.Enter));
checkBinding("Left (centre)", InputSettingsStrings.ActionHasNoKeyBinding.ToString());
checkBinding("Left (rim)", "M1");
}
[Test] [Test]
public void TestBindingConflictCausedByResetToDefaultOfSingleRow() public void TestBindingConflictCausedByResetToDefaultOfSingleRow()
{ {

View File

@ -77,21 +77,21 @@ namespace osu.Game.Tests.Visual.UserInterface
new OsuMenuItem(@"Some option"), new OsuMenuItem(@"Some option"),
new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted), new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted),
new OsuMenuItem(@"Another option"), new OsuMenuItem(@"Another option"),
new OsuMenuItem(@"Nested option >") new OsuMenuItem(@"Nested option")
{ {
Items = new MenuItem[] Items = new MenuItem[]
{ {
new OsuMenuItem(@"Sub-One"), new OsuMenuItem(@"Sub-One"),
new OsuMenuItem(@"Sub-Two"), new OsuMenuItem(@"Sub-Two"),
new OsuMenuItem(@"Sub-Three"), new OsuMenuItem(@"Sub-Three"),
new OsuMenuItem(@"Sub-Nested option >") new OsuMenuItem(@"Sub-Nested option")
{ {
Items = new MenuItem[] Items = new MenuItem[]
{ {
new OsuMenuItem(@"Double Sub-One"), new OsuMenuItem(@"Double Sub-One"),
new OsuMenuItem(@"Double Sub-Two"), new OsuMenuItem(@"Double Sub-Two"),
new OsuMenuItem(@"Double Sub-Three"), new OsuMenuItem(@"Double Sub-Three"),
new OsuMenuItem(@"Sub-Sub-Nested option >") new OsuMenuItem(@"Sub-Sub-Nested option")
{ {
Items = new MenuItem[] Items = new MenuItem[]
{ {

View File

@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Files = { new RealmNamedFileUsage(new RealmFile { Hash = $"{i}" }, string.Empty) } Files = { new RealmNamedFileUsage(new RealmFile { Hash = $"{i}" }, string.Empty) }
}; };
importedScores.Add(scoreManager.Import(score).Value); importedScores.Add(scoreManager.Import(score)!.Value);
} }
}); });
}); });

View File

@ -123,6 +123,43 @@ namespace osu.Game.Tests.Visual.UserInterface
assertSelectedModsEquivalentTo(new Mod[] { new OsuModTouchDevice(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); assertSelectedModsEquivalentTo(new Mod[] { new OsuModTouchDevice(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
} }
[Test]
public void TestSystemModsNotPreservedIfIncompatibleWithPresetMods()
{
ModPresetPanel? panel = null;
AddStep("create panel", () => Child = panel = new ModPresetPanel(new ModPreset
{
Name = "Autopilot included",
Description = "no way",
Mods = new Mod[]
{
new OsuModAutopilot()
},
Ruleset = new OsuRuleset().RulesetInfo
}.ToLiveUnmanaged())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f
});
AddStep("Add touch device to selected mods", () => SelectedMods.Value = new Mod[] { new OsuModTouchDevice() });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
// touch device should be removed due to incompatibility with autopilot.
assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutopilot() });
AddStep("deactivate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(Array.Empty<Mod>());
// just for test purposes, can't/shouldn't happen in reality
AddStep("Add score v2 to selected mod", () => SelectedMods.Value = new Mod[] { new ModScoreV2() });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModAutopilot(), new ModScoreV2() });
}
private void assertSelectedModsEquivalentTo(IEnumerable<Mod> mods) private void assertSelectedModsEquivalentTo(IEnumerable<Mod> mods)
=> AddAssert("selected mods changed correctly", () => new HashSet<Mod>(SelectedMods.Value).SetEquals(mods)); => AddAssert("selected mods changed correctly", () => new HashSet<Mod>(SelectedMods.Value).SetEquals(mods));

View File

@ -612,6 +612,23 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus); AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus);
} }
[Test]
public void TestSearchBoxFocusToggleRespondsToExternalChanges()
{
AddStep("text search does not start active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false));
createScreen();
AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("unfocus search text box externally", () => InputManager.ChangeFocus(null));
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
}
[Test] [Test]
public void TestTextSearchDoesNotBlockCustomisationPanelKeyboardInteractions() public void TestTextSearchDoesNotBlockCustomisationPanelKeyboardInteractions()
{ {

View File

@ -5,7 +5,9 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -20,9 +22,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private NowPlayingOverlay nowPlayingOverlay; private NowPlayingOverlay nowPlayingOverlay;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(FrameworkConfigManager frameworkConfig)
{ {
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); AddToggleStep("toggle unicode", v => frameworkConfig.SetValue(FrameworkSetting.ShowUnicode, v));
nowPlayingOverlay = new NowPlayingOverlay nowPlayingOverlay = new NowPlayingOverlay
{ {
@ -37,9 +39,38 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestShowHideDisable() public void TestShowHideDisable()
{ {
AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
AddStep(@"show", () => nowPlayingOverlay.Show()); AddStep(@"show", () => nowPlayingOverlay.Show());
AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state);
AddStep(@"hide", () => nowPlayingOverlay.Hide()); AddStep(@"hide", () => nowPlayingOverlay.Hide());
} }
[Test]
public void TestLongMetadata()
{
AddStep(@"set metadata within tolerance", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
Metadata =
{
Artist = "very very very very very very very very very very verry long artist",
ArtistUnicode = "very very very very very very very very very very verry long artist unicode",
Title = "very very very very very verry long title",
TitleUnicode = "very very very very very verry long title unicode",
}
}));
AddStep(@"set metadata outside bounds", () => Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
Metadata =
{
Artist = "very very very very very very very very very very verrry long artist",
ArtistUnicode = "not very long artist unicode",
Title = "very very very very very verrry long title",
TitleUnicode = "not very long title unicode",
}
}));
AddStep(@"show", () => nowPlayingOverlay.Show());
}
} }
} }

View File

@ -114,6 +114,51 @@ namespace osu.Game.Tests.Visual.UserInterface
=> AddAssert($"state is {expected}", () => state.Value == expected); => AddAssert($"state is {expected}", () => state.Value == expected);
} }
[Test]
public void TestItemRespondsToRightClick()
{
OsuMenu menu = null;
Bindable<TernaryState> state = new Bindable<TernaryState>(TernaryState.Indeterminate);
AddStep("create menu", () =>
{
state.Value = TernaryState.Indeterminate;
Child = menu = new OsuMenu(Direction.Vertical, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Items = new[]
{
new TernaryStateToggleMenuItem("First"),
new TernaryStateToggleMenuItem("Second") { State = { BindTarget = state } },
new TernaryStateToggleMenuItem("Third") { State = { Value = TernaryState.True } },
}
};
});
checkState(TernaryState.Indeterminate);
click();
checkState(TernaryState.True);
click();
checkState(TernaryState.False);
AddStep("change state via bindable", () => state.Value = TernaryState.True);
void click() =>
AddStep("click", () =>
{
InputManager.MoveMouseTo(menu.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Right);
});
void checkState(TernaryState expected)
=> AddAssert($"state is {expected}", () => state.Value == expected);
}
[Test] [Test]
public void TestCustomState() public void TestCustomState()
{ {

View File

@ -63,6 +63,8 @@ namespace osu.Game.Beatmaps
public List<BreakPeriod> Breaks { get; set; } = new List<BreakPeriod>(); public List<BreakPeriod> Breaks { get; set; } = new List<BreakPeriod>();
public List<string> UnhandledEventLines { get; set; } = new List<string>();
[JsonIgnore] [JsonIgnore]
public double TotalBreakTime => Breaks.Sum(b => b.Duration); public double TotalBreakTime => Breaks.Sum(b => b.Duration);

View File

@ -66,6 +66,7 @@ namespace osu.Game.Beatmaps
beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.ControlPointInfo = original.ControlPointInfo;
beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks; beatmap.Breaks = original.Breaks;
beatmap.UnhandledEventLines = original.UnhandledEventLines;
return beatmap; return beatmap;
} }

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -21,6 +22,8 @@ using osu.Game.Utils;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
[SuppressMessage("ReSharper", "StringLiteralTypo")]
[SuppressMessage("ReSharper", "CommentTypo")]
public partial class BundledBeatmapDownloader : CompositeDrawable public partial class BundledBeatmapDownloader : CompositeDrawable
{ {
private readonly bool shouldPostNotifications; private readonly bool shouldPostNotifications;
@ -50,7 +53,7 @@ namespace osu.Game.Beatmaps.Drawables
{ {
queueDownloads(always_bundled_beatmaps); queueDownloads(always_bundled_beatmaps);
queueDownloads(bundled_osu, 8); queueDownloads(bundled_osu, 6);
queueDownloads(bundled_taiko, 3); queueDownloads(bundled_taiko, 3);
queueDownloads(bundled_catch, 3); queueDownloads(bundled_catch, 3);
queueDownloads(bundled_mania, 3); queueDownloads(bundled_mania, 3);
@ -128,6 +131,26 @@ namespace osu.Game.Beatmaps.Drawables
} }
} }
/*
* criteria for bundled maps (managed by pishifat)
*
* auto:
* - licensed song
* - includes ENHI diffs
* - between 60s and 240s
*
* manual:
* - bg is explicitly permitted as okay to use. lots of artists say some variation of "it's ok for personal use/non-commercial use/with credit"
* (which is prob fine when maps are presented as user-generated content), but for a new osu! player, it's easy to assume bundled maps are
* commercial content like other rhythm games, so it's best to be cautious about using not-explicitly-permitted artwork.
*
* - no ai/thirst bgs
* - no controversial/explicit song content or titles
* - no repeating bundled songs (within each mode)
* - no songs that are relatively low production value
* - no songs with limited accessibility (annoying high pitch vocals, noise rock, etc)
*/
private const string tutorial_filename = "1011011 nekodex - new beginnings.osz"; private const string tutorial_filename = "1011011 nekodex - new beginnings.osz";
/// <summary> /// <summary>
@ -135,215 +158,312 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary> /// </summary>
private static readonly string[] always_bundled_beatmaps = private static readonly string[] always_bundled_beatmaps =
{ {
// This thing is 40mb, I'm not sure we want it here... // winner of https://osu.ppy.sh/home/news/2013-09-06-osu-monthly-beatmapping-contest-1
@"123593 Rostik - Liquid (Paul Rosenthal Remix).osz",
// winner of https://osu.ppy.sh/home/news/2013-10-28-monthly-beatmapping-contest-2-submissions-open
@"140662 cYsmix feat. Emmy - Tear Rain.osz",
// winner of https://osu.ppy.sh/home/news/2013-12-15-monthly-beatmapping-contest-3-submissions-open
@"151878 Chasers - Lost.osz",
// winner of https://osu.ppy.sh/home/news/2014-02-14-monthly-beatmapping-contest-4-submissions-now
@"163112 Kuba Oms - My Love.osz",
// winner of https://osu.ppy.sh/home/news/2014-05-07-monthly-beatmapping-contest-5-submissions-now
@"190390 Rameses B - Flaklypa.osz",
// winner of https://osu.ppy.sh/home/news/2014-09-24-monthly-beatmapping-contest-7
@"241526 Soleily - Renatus.osz",
// winner of https://osu.ppy.sh/home/news/2015-02-11-monthly-beatmapping-contest-8
@"299224 raja - the light.osz",
// winner of https://osu.ppy.sh/home/news/2015-04-13-monthly-beatmapping-contest-9-taiko-only
@"319473 Furries in a Blender - Storm World.osz",
// winner of https://osu.ppy.sh/home/news/2015-06-15-monthly-beatmapping-contest-10-ctb-only
@"342751 Hylian Lemon - Foresight Is for Losers.osz",
// winner of https://osu.ppy.sh/home/news/2015-08-22-monthly-beatmapping-contest-11-mania-only
@"385056 Toni Leys - Dragon Valley (Toni Leys Remix feat. Esteban Bellucci).osz",
// winner of https://osu.ppy.sh/home/news/2016-03-04-beatmapping-contest-12-osu
@"456054 IAHN - Candy Luv (Short Ver.).osz",
// winner of https://osu.ppy.sh/home/news/2020-11-30-a-labour-of-love
// (this thing is 40mb, I'm not sure if we want it here...)
@"1388906 Raphlesia & BilliumMoto - My Love.osz", @"1388906 Raphlesia & BilliumMoto - My Love.osz",
// Winner of Triangles mapping competition: https://osu.ppy.sh/home/news/2022-10-06-results-triangles // winner of https://osu.ppy.sh/home/news/2022-05-31-triangles
@"1841885 cYsmix - triangles.osz", @"1841885 cYsmix - triangles.osz",
// winner of https://osu.ppy.sh/home/news/2023-02-01-twin-trials-contest-beatmapping-phase
@"1971987 James Landino - Aresene's Bazaar.osz",
}; };
private static readonly string[] bundled_osu = private static readonly string[] bundled_osu =
{ {
"682286 Yuyoyuppe - Emerald Galaxy.osz", @"682286 Yuyoyuppe - Emerald Galaxy.osz",
"682287 baker - For a Dead Girl+.osz", @"682287 baker - For a Dead Girl+.osz",
"682289 Hige Driver - I Wanna Feel Your Love (feat. shully).osz", @"682595 baker - Kimi ga Kimi ga -vocanico remix-.osz",
"682290 Hige Driver - Miracle Sugite Yabai (feat. shully).osz", @"1048705 Thaehan - Never Give Up.osz",
"682416 Hige Driver - Palette.osz", @"1050185 Carpool Tunnel - Hooked Again.osz",
"682595 baker - Kimi ga Kimi ga -vocanico remix-.osz", @"1052846 Carpool Tunnel - Impressions.osz",
"716211 yuki. - Spring Signal.osz", @"1062477 Ricky Montgomery - Line Without a Hook.osz",
"716213 dark cat - BUBBLE TEA (feat. juu & cinders).osz", @"1081119 Celldweller - Pulsar.osz",
"716215 LukHash - CLONED.osz", @"1086289 Frums - 24eeev0-$.osz",
"716219 IAHN - Snowdrop.osz", @"1133317 PUP - Free At Last.osz",
"716249 *namirin - Senaka Awase no Kuukyo (with Kakichoco).osz", @"1171188 PUP - Full Blown Meltdown.osz",
"716390 sakuraburst - SHA.osz", @"1177043 PUP - My Life Is Over And I Couldn't Be Happier.osz",
"716441 Fractal Dreamers - Paradigm Shift.osz", @"1250387 Circle of Dust - Humanarchy (Cut Ver.).osz",
"729808 Thaehan - Leprechaun.osz", @"1255411 Wisp X - Somewhere I'd Rather Be.osz",
"751771 Cranky - Hanaarashi.osz", @"1320298 nekodex - Little Drummer Girl.osz",
"751772 Cranky - Ran.osz", @"1323877 Masahiro ""Godspeed"" Aoki - Blaze.osz",
"751773 Cranky - Feline, the White....osz", @"1342280 Minagu feat. Aitsuki Nakuru - Theater Endroll.osz",
"751774 Function Phantom - Variable.osz", @"1356447 SECONDWALL - Boku wa Boku de shika Nakute.osz",
"751779 Rin - Daishibyo set 14 ~ Sado no Futatsuiwa.osz", @"1368054 SECONDWALL - Shooting Star.osz",
"751782 Fractal Dreamers - Fata Morgana.osz", @"1398580 La priere - Senjou no Utahime.osz",
"751785 Cranky - Chandelier - King.osz", @"1403962 m108 - Sunflower.osz",
"751846 Fractal Dreamers - Celestial Horizon.osz", @"1405913 fiend - FEVER DREAM (feat. yzzyx).osz",
"751866 Rin - Moriya set 08 ReEdit ~ Youkai no Yama.osz", @"1409184 Omoi - Hey William (New Translation).osz",
"751894 Fractal Dreamers - Blue Haven.osz", @"1413418 URBANGARDE - KAMING OUT (Cut Ver.).osz",
"751896 Cranky - Rave 2 Rave.osz", @"1417793 P4koo (NONE) - Sogaikan Utopia.osz",
"751932 Cranky - La fuite des jours.osz", @"1428384 DUAL ALTER WORLD - Veracila.osz",
"751972 Cranky - CHASER.osz", @"1442963 PUP - DVP.osz",
"779173 Thaehan - Superpower.osz", @"1460370 Sound Souler - Empty Stars.osz",
"780932 VINXIS - A Centralized View.osz", @"1485184 Koven - Love Wins Again.osz",
"785572 S3RL - I'll See You Again (feat. Chi Chi).osz", @"1496811 T & Sugah - Wicked Days (Cut Ver.).osz",
"785650 yuki. feat. setsunan - Hello! World.osz", @"1501511 Masahiro ""Godspeed"" Aoki - Frostbite (Cut Ver.).osz",
"785677 Dictate - Militant.osz", @"1511518 T & Sugah X Zazu - Lost On My Own (Cut Ver.).osz",
"785731 S3RL - Catchit (Radio Edit).osz", @"1516617 wotoha - Digital Life Hacker.osz",
"785774 LukHash - GLITCH.osz", @"1524273 Michael Cera Palin - Admiral.osz",
"786498 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz", @"1564234 P4koo - Fly High (feat. rerone).osz",
"789374 Pulse - LP.osz", @"1572918 Lexurus - Take Me Away (Cut Ver.).osz",
"789528 James Portland - Sky.osz", @"1577313 Kurubukko - The 84th Flight.osz",
"789529 Lexurus - Gravity.osz", @"1587839 Amidst - Droplet.osz",
"789544 Andromedik - Invasion.osz", @"1595193 BlackY - Sakura Ranman Cleopatra.osz",
"789905 Gourski x Himmes - Silence.osz", @"1667560 xi - FREEDOM DiVE.osz",
"791667 cYsmix - Babaroque (Short Ver.).osz", @"1668789 City Girl - L2M (feat. Kelsey Kuan).osz",
"791798 cYsmix - Behind the Walls.osz", @"1672934 xi - Parousia.osz",
"791845 cYsmix - Little Knight.osz", @"1673457 Boom Kitty - Any Other Way (feat. Ivy Marie).osz",
"792241 cYsmix - Eden.osz", @"1685122 xi - Time files.osz",
"792396 cYsmix - The Ballad of a Mindless Girl.osz", @"1689372 NIWASHI - Y.osz",
"795432 Phonetic - Journey.osz", @"1729551 JOYLESS - Dream.osz",
"831322 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz", @"1742868 Ritorikal - Synergy.osz",
"847764 Cranky - Crocus.osz", @"1757511 KINEMA106 - KARASU.osz",
"847776 Culprate & Joe Ford - Gaucho.osz", @"1778169 Ricky Montgomery - Cabo.osz",
"847812 J. Pachelbel - Canon (Cranky Remix).osz", @"1848184 FRASER EDWARDS - Ruination.osz",
"847900 Cranky - Time Alter.osz", @"1862574 Pegboard Nerds - Try This (Cut Ver.).osz",
"847930 LukHash - 8BIT FAIRY TALE.osz", @"1873680 happy30 - You spin my world.osz",
"848003 Culprate - Aurora.osz", @"1890055 A.SAKA - Mutsuki Akari no Yuki.osz",
"848068 nanobii - popsicle beach.osz", @"1911933 Marmalade butcher - Waltz for Chroma (feat. Natsushiro Takaaki).osz",
"848090 Trial & Error - DAI*TAN SENSATION feat. Nanahira, Mii, Aitsuki Nakuru (Short Ver.).osz", @"1940007 Mili - Ga1ahad and Scientific Witchery.osz",
"848259 Culprate & Skorpion - Jester.osz", @"1948970 Shadren - You're Here Forever.osz",
"848976 Dictate - Treason.osz", @"1967856 Annabel - alpine blue.osz",
"851543 Culprate - Florn.osz", @"1969316 Silentroom - NULCTRL.osz",
"864748 Thaehan - Angry Birds Epic (Remix).osz", @"1978614 Krimek - Idyllic World.osz",
"873667 OISHII - ONIGIRI FREEWAY.osz", @"1991315 Feint - Tower Of Heaven (You Are Slaves) (Cut Ver.).osz",
"876227 Culprate, Keota & Sophie Meiers - Mechanic Heartbeat.osz", @"1997470 tephe - Genjitsu Escape.osz",
"880487 cYsmix - Peer Gynt.osz", @"1999116 soowamisu - .vaporcore.osz",
"883088 Wisp X - Somewhere I'd Rather Be.osz", @"2010589 Junk - Yellow Smile (bms edit).osz",
"891333 HyuN - White Aura.osz", @"2022054 Yokomin - STINGER.osz",
"891334 HyuN - Wild Card.osz", @"2025686 Aice room - For U.osz",
"891337 HyuN feat. LyuU - Cross Over.osz", @"2035357 C-Show feat. Ishizawa Yukari - Border Line.osz",
"891338 HyuN & Ritoru - Apocalypse in Love.osz", @"2039403 SECONDWALL - Freedom.osz",
"891339 HyuN feat. Ato - Asu wa Ame ga Yamukara.osz", @"2046487 Rameses B - Against the Grain (feat. Veela).osz",
"891345 HyuN - Infinity Heaven.osz", @"2052201 ColBreakz & Vizzen - Remember.osz",
"891348 HyuN - Guitian.osz", @"2055535 Sephid - Thunderstrike 1988.osz",
"891356 HyuN - Legend of Genesis.osz", @"2057584 SAMString - Ataraxia.osz",
"891366 HyuN - Illusion of Inflict.osz", @"2067270 Blue Stahli - The Fall.osz",
"891417 HyuN feat. Yu-A - My life is for you.osz", @"2075039 garlagan - Skyless.osz",
"891441 HyuN - You'Re aRleAdY dEAd.osz", @"2079089 Hamu feat. yuiko - Innocent Letter.osz",
"891632 HyuN feat. YURI - Disorder.osz", @"2082895 FATE GEAR - Heart's Grave.osz",
"891712 HyuN - Tokyo's Starlight.osz", @"2085974 HoneyComeBear - Twilight.osz",
"901091 *namirin - Ciel etoile.osz", @"2094934 F.O.O.L & Laura Brehm - Waking Up.osz",
"916990 *namirin - Koishiteiku Planet.osz", @"2097481 Mameyudoufu - Wave feat. Aitsuki Nakuru.osz",
"929284 tieff - Sense of Nostalgia.osz", @"2106075 MYUKKE. - The 89's Momentum.osz",
"933940 Ben Briggs - Yes (Maybe).osz", @"2117392 t+pazolite & Komiya Mao - Elustametat.osz",
"934415 Ben Briggs - Fearless Living.osz", @"2123533 LeaF - Calamity Fortune.osz",
"934627 Ben Briggs - New Game Plus.osz", @"2143876 Alkome - Your Voice.osz",
"934666 Ben Briggs - Wave Island.osz", @"2145826 Sephid - Cross-D Skyline.osz",
"936126 siromaru + cranky - conflict.osz", @"2153172 Emiru no Aishita Tsukiyo ni Dai San Gensou Kyoku wo - Eternal Bliss.osz",
"940377 onumi - ARROGANCE.osz",
"940597 tieff - Take Your Swimsuit.osz",
"941085 tieff - Our Story.osz",
"949297 tieff - Sunflower.osz",
"952380 Ben Briggs - Why Are We Yelling.osz",
"954272 *namirin - Kanzen Shouri*Esper Girl.osz",
"955866 KIRA & Heartbreaker - B.B.F (feat. Hatsune Miku & Kagamine Rin).osz",
"961320 Kuba Oms - All In All.osz",
"964553 The Flashbulb - You Take the World's Weight Away.osz",
"965651 Fractal Dreamers - Ad Astra.osz",
"966225 The Flashbulb - Passage D.osz",
"966324 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz",
"972810 James Landino & Kabuki - Birdsong.osz",
"972932 James Landino - Hide And Seek.osz",
"977276 The Flashbulb - Mellann.osz",
"981616 *namirin - Mizutamari Tobikoete (with Nanahira).osz",
"985788 Loki - Wizard's Tower.osz",
"996628 OISHII - ONIGIRI FREEWAY.osz",
"996898 HyuN - White Aura.osz",
"1003554 yuki. - Nadeshiko Sensation.osz",
"1014936 Thaehan - Bwa !.osz",
"1019827 UNDEAD CORPORATION - Sad Dream.osz",
"1020213 Creo - Idolize.osz",
"1021450 Thaehan - Chiptune & Baroque.osz",
}; };
private static readonly string[] bundled_taiko = private static readonly string[] bundled_taiko =
{ {
"707824 Fractal Dreamers - Fortuna Redux.osz", "1048153 Chroma - [@__@].osz",
"789553 Cranky - Ran.osz", "1229307 Venetian Snares - Shaky Sometimes.osz",
"827822 Function Phantom - Neuronecia.osz", "1236083 meganeko - Sirius A (osu! edit).osz",
"847323 Nakanojojo - Bittersweet (feat. Kuishinboakachan a.k.a Kiato).osz", "1248594 Noisia - Anomaly.osz",
"847433 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz", "1272851 siqlo - One Way Street.osz",
"847576 dark cat - hot chocolate.osz", "1290736 Kola Kid - good old times.osz",
"847957 Wisp X - Final Moments.osz", "1318825 SECONDWALL - Light.osz",
"876282 VINXIS - Greetings.osz", "1320872 MYUKKE. - The 89's Momentum.osz",
"876648 Thaehan - Angry Birds Epic (Remix).osz", "1337389 cute girls doing cute things - Main Heroine.osz",
"877069 IAHN - Transform (Original Mix).osz", "1397782 Reku Mochizuki - Yorixiro.osz",
"877496 Thaehan - Leprechaun.osz", "1407228 II-L - VANGUARD-1.osz",
"877935 Thaehan - Overpowered.osz", "1422686 II-L - VANGUARD-2.osz",
"878344 yuki. - Be Your Light.osz", "1429217 Street - Phi.osz",
"918446 VINXIS - Facade.osz", "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz",
"918903 LukHash - Ghosts.osz", "1447478 Cres. - End Time.osz",
"919251 *namirin - Hitokoto no Kyori.osz", "1449942 m108 - Crescent Sakura.osz",
"919704 S3RL - I Will Pick You Up (feat. Tamika).osz", "1463778 MuryokuP - A tree without a branch.osz",
"921535 SOOOO - Raven Haven.osz", "1465152 fiend - Fever Dream (feat. yzzyx).osz",
"927206 *namirin - Kanzen Shouri*Esper Girl.osz", "1472397 MYUKKE. - Boudica.osz",
"927544 Camellia feat. Nanahira - Kansoku Eisei.osz", "1488148 Aoi vs. siqlo - Hacktivism.osz",
"930806 Nakanojojo - Pararara (feat. Amekoya).osz", "1522733 wotoha - Digital Life Hacker.osz",
"931741 Camellia - Quaoar.osz", "1540010 Marmalade butcher - Floccinaucinihilipilification.osz",
"935699 Rin - Mythic set ~ Heart-Stirring Urban Legends.osz", "1584690 MYUKKE. - AKKERA-COUNTRY-BOY.osz",
"935732 Thaehan - Yuujou.osz", "1608857 BLOOD STAIN CHILD - S.O.P.H.I.A.osz",
"941145 Function Phantom - Euclid.osz", "1609365 Reku Mochizuki - Faith of Eastward.osz",
"942334 Dictate - Cauldron.osz", "1622545 METAROOM - I - DINKI THE STARGUIDE.osz",
"946540 nanobii - astral blast.osz", "1629336 METAROOM - PINK ORIGINS.osz",
"948844 Rin - Kishinjou set 01 ~ Mist Lake.osz", "1644680 Neko Hacker - Pictures feat. 4s4ki.osz",
"949122 Wisp X - Petal.osz", "1650835 RiraN - Ready For The Madness.osz",
"951618 Rin - Kishinjou set 02 ~ Mermaid from the Uncharted Land.osz", "1661508 PTB10 - Starfall.osz",
"957412 Rin - Lunatic set 16 ~ The Space Shrine Maiden Returns Home.osz", "1671987 xi - World Fragments II.osz",
"961335 Thaehan - Insert Coin.osz", "1703065 tokiwa - wasurena feat. Sennzai.osz",
"965178 The Flashbulb - DIDJ PVC.osz", "1703527 tokiwa feat. Nakamura Sanso - Kotodama Refrain.osz",
"966087 The Flashbulb - Creep.osz", "1704340 A-One feat. Shihori - Magic Girl !!.osz",
"966277 The Flashbulb - Amen Iraq.osz", "1712783 xi - Parousia.osz",
"966407 LukHash - ROOM 12.osz", "1718774 Harumaki Gohan - Suisei ni Nareta nara.osz",
"966451 The Flashbulb - Six Acid Strings.osz", "1719687 EmoCosine - Love Kills U.osz",
"972301 BilliumMoto - four veiled stars.osz", "1733940 WHITEFISTS feat. Sennzai - Paralyzed Ash.osz",
"973173 nanobii - popsicle beach.osz", "1734692 EmoCosine - Cutter.osz",
"973954 BilliumMoto - Rocky Buinne (Short Ver.).osz", "1739529 luvlxckdown - tbh i dont like being social.osz",
"975435 BilliumMoto - life flashes before weeb eyes.osz", "1756970 Kurubukko vs. yukitani - Minamichita EVOLVED.osz",
"978759 L. V. Beethoven - Moonlight Sonata (Cranky Remix).osz", "1762209 Marmalade butcher - Immortality Math Club.osz",
"982559 BilliumMoto - HDHR.osz", "1765720 ZxNX - FORTALiCE.osz",
"984361 The Flashbulb - Ninedump.osz", "1786165 NILFRUITS - Arandano.osz",
"1023681 Inferi - The Ruin of Mankind.osz", "1787258 SAMString - Night Fighter.osz",
"1034358 ALEPH - The Evil Spirit.osz", "1791462 ZxNX - Schadenfreude.osz",
"1037567 ALEPH - Scintillations.osz", "1793821 Kobaryo - The Lightning Sword.osz",
"1796440 kuru x miraie - re:start.osz",
"1799285 Origami Angel - 666 Flags.osz",
"1812415 nanobii - Rainbow Road.osz",
"1814682 NIWASHI - Y.osz",
"1818361 meganeko - Feral (osu! edit).osz",
"1818924 fiend - Disconnect.osz",
"1838730 Pegboard Nerds - Disconnected.osz",
"1854710 Blaster & Extra Terra - Spacecraft (Cut Ver.).osz",
"1859322 Hino Isuka - Delightness Brightness.osz",
"1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz",
"1884578 Neko Hacker - People People feat. Nanahira.osz",
"1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz",
"1905582 KINEMA106 - Fly Away (Cut Ver.).osz",
"1934686 ARForest - Rainbow Magic!!.osz",
"1963076 METAROOM - S.N.U.F.F.Y.osz",
"1968973 Stars Hollow - Out the Sunroof..osz",
"1971951 James Landino - Shiba Paradise.osz",
"1972518 Toromaru - Sleight of Hand.osz",
"1982302 KINEMA106 - INVITE.osz",
"1983475 KNOWER - The Government Knows.osz",
"2010165 Junk - Yellow Smile (bms edit).osz",
"2022737 Andora - Euphoria (feat. WaMi).osz",
"2025023 tephe - Genjitsu Escape.osz",
"2052754 P4koo - 8th:Planet ~Re:search~.osz",
"2054122 Raimukun - Myths Orbis.osz",
"2121470 Raimukun - Nyarlathotep's Dreamland.osz",
"2122284 Agressor Bunx - Tornado (Cut Ver.).osz",
"2125034 Agressor Bunx - Acid Mirage (Cut Ver.).osz",
"2136263 Se-U-Ra - Cris Fortress.osz",
}; };
private static readonly string[] bundled_catch = private static readonly string[] bundled_catch =
{ {
"554256 Helblinde - When Time Sleeps.osz", @"693123 yuki. - Nadeshiko Sensation.osz",
"693123 yuki. - Nadeshiko Sensation.osz", @"833719 FOLiACETATE - Heterochromia Iridis.osz",
"767009 OISHII - PIZZA PLAZA.osz", @"981762 siromaru + cranky - conflict.osz",
"767346 Thaehan - Bwa !.osz", @"1008600 LukHash - WHEN AN ANGEL DIES.osz",
"815162 VINXIS - Greetings.osz", @"1071294 dark cat - pursuit of happiness.osz",
"840964 cYsmix - Breeze.osz", @"1102115 meganeko - Nova.osz",
"932657 Wisp X - Eventide.osz", @"1115500 Chopin - Etude Op. 25, No. 12 (meganeko Remix).osz",
"933700 onumi - CONFUSION PART ONE.osz", @"1128274 LeaF - Wizdomiot.osz",
"933984 onumi - PERSONALITY.osz", @"1141049 HyuN feat. JeeE - Fallen Angel.osz",
"934785 onumi - FAKE.osz", @"1148215 Zekk - Fluctuation.osz",
"936545 onumi - REGRET PART ONE.osz", @"1151833 ginkiha - nightfall.osz",
"943803 Fractal Dreamers - Everything for a Dream.osz", @"1158124 PUP - Dark Days.osz",
"943876 S3RL - I Will Pick You Up (feat. Tamika).osz", @"1184890 IAHN - Transform (Original Mix).osz",
"946773 Trial & Error - DREAMING COLOR (Short Ver.).osz", @"1195922 Disasterpeace - Home.osz",
"955808 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI (Short Ver.).osz", @"1197461 MIMI - Nanimo nai Youna.osz",
"957808 Fractal Dreamers - Module_410.osz", @"1197924 Camellia feat. Nanahira - Looking For A New Adventure.osz",
"957842 antiPLUR - One Life Left to Live.osz", @"1203594 ginkiha - Anemoi.osz",
"965730 The Flashbulb - Lawn Wake IV (Black).osz", @"1211572 MIMI - Lapis Lazuli.osz",
"966240 Creo - Challenger.osz", @"1231601 Lime - Harmony.osz",
"968232 Rin - Lunatic set 15 ~ The Moon as Seen from the Shrine.osz", @"1240162 P4koo - 8th:Planet ~Re:search~.osz",
"972302 VINXIS - A Centralized View.osz", @"1246000 Zekk - Calling.osz",
"972887 HyuN - Illusion of Inflict.osz", @"1249928 Thaehan - Yuujou.osz",
"1008600 LukHash - WHEN AN ANGEL DIES.osz", @"1258751 Umeboshi Chazuke - ICHIBANBOSHI*ROCKET.osz",
"1032103 LukHash - H8 U.osz", @"1264818 Umeboshi Chazuke - Panic! Pop'n! Picnic! (2019 REMASTER).osz",
@"1280183 IAHN - Mad Halloween.osz",
@"1303201 Umeboshi Chazuke - Run*2 Run To You!!.osz",
@"1328918 Kobaryo - Theme for Psychopath Justice.osz",
@"1338215 Lime - Renai Syndrome.osz",
@"1338796 uma vs. Morimori Atsushi - Re:End of a Dream.osz",
@"1340492 MYUKKE. - The 89's Momentum.osz",
@"1393933 Mastermind (xi+nora2r) - Dreadnought.osz",
@"1400205 m108 - XIII Charlotte.osz",
@"1471328 Lime - Chronomia.osz",
@"1503591 Origami Angel - The Title Track.osz",
@"1524173 litmus* as Ester - Krave.osz",
@"1541235 Getty vs. DJ DiA - Grayed Out -Antifront-.osz",
@"1554250 Shawn Wasabi - Otter Pop (feat. Hollis).osz",
@"1583461 Sound Souler - Absent Color.osz",
@"1638487 tokiwa - wasurena feat. Sennzai.osz",
@"1698949 ZxNX - Schadenfreude.osz",
@"1704324 xi - Time files.osz",
@"1756405 Fractal Dreamers - Kingdom of Silence.osz",
@"1769575 cYsmix - Peer Gynt.osz",
@"1770054 Ardolf - Split.osz",
@"1772648 in love with a ghost - interdimensional portal leading to a cute place feat. snail's house.osz",
@"1776379 in love with a ghost - i thought we were lovers w/ basil.osz",
@"1779476 URBANGARDE - KIMI WA OKUMAGASO.osz",
@"1789435 xi - Parousia.osz",
@"1794190 Se-U-Ra - The Endless for Traveler.osz",
@"1799889 Waterflame - Ricochet Love.osz",
@"1816401 Gram vs. Yooh - Apocalypse.osz",
@"1826327 -45 - Total Eclipse of The Sun.osz",
@"1830796 xi - Halcyon.osz",
@"1924231 Mili - Nine Point Eight.osz",
@"1952903 Cres. - End Time.osz",
@"1970946 Good Kid - Slingshot.osz",
@"1982063 linear ring - enchanted love.osz",
@"2000438 Toromaru - Erinyes.osz",
@"2124277 II-L - VANGUARD-3.osz",
@"2147529 Nashimoto Ui - AaAaAaAAaAaAAa (Cut Ver.).osz",
}; };
private static readonly string[] bundled_mania = private static readonly string[] bundled_mania =
{ {
"943516 antiPLUR - Clockwork Spooks.osz", @"1008419 BilliumMoto - Four Veiled Stars.osz",
"946394 VINXIS - Three Times The Original Charm.osz", @"1025170 Frums - We Want To Run.osz",
"966408 antiPLUR - One Life Left to Live.osz", @"1092856 F-777 - Viking Arena.osz",
"971561 antiPLUR - Runengon.osz", @"1139247 O2i3 - Heart Function.osz",
"983864 James Landino - Shiba Island.osz", @"1154007 LeaF - ATHAZA.osz",
"989512 BilliumMoto - 1xMISS.osz", @"1170054 Zekk - Fallen.osz",
"994104 James Landino - Reaction feat. Slyleaf.osz", @"1212132 Street - Koiyamai (TV Size).osz",
"1003217 nekodex - circles!.osz", @"1226466 Se-U-Ra - Elif to Shiro Kura no Yoru -Called-.osz",
"1009907 James Landino & Kabuki - Birdsong.osz", @"1247210 Frums - Credits.osz",
"1015169 Thaehan - Insert Coin.osz", @"1254196 ARForest - Regret.osz",
@"1258829 Umeboshi Chazuke - Cineraria.osz",
@"1300398 ARForest - The Last Page.osz",
@"1305627 Frums - Star of the COME ON!!.osz",
@"1348806 Se-U-Ra - LOA2.osz",
@"1375449 yuki. - Nadeshiko Sensation.osz",
@"1448292 Cres. - End Time.osz",
@"1479741 Reku Mochizuki - FORViDDEN ENERZY -Fataldoze-.osz",
@"1494747 Fractal Dreamers - Whispers from a Distant Star.osz",
@"1505336 litmus* - Rush-More.osz",
@"1508963 ARForest - Rainbow Magic!!.osz",
@"1727126 Chroma - Strange Inventor.osz",
@"1737101 ZxNX - FORTALiCE.osz",
@"1740952 Sobrem x Silentroom - Random.osz",
@"1756251 Plum - Mad Piano Party.osz",
@"1909163 Frums - theyaremanycolors.osz",
@"1916285 siromaru + cranky - conflict.osz",
@"1948972 Ardolf - Split.osz",
@"1957138 GLORYHAMMER - Rise Of The Chaos Wizards.osz",
@"1972411 James Landino - Shiba Paradise.osz",
@"1978179 Andora - Flicker (feat. RANASOL).osz",
@"1987180 cygnus - The Evolution of War.osz",
@"1994458 tephe - Genjitsu Escape.osz",
@"1999339 Aice room - Nyan Nyan Dive (EmoCosine Remix).osz",
@"2015361 HoneyComeBear - Rainy Girl.osz",
@"2028108 HyuN - Infinity Heaven.osz",
@"2055329 miraie & blackwinterwells - facade.osz",
@"2069877 Sephid - Thunderstrike 1988.osz",
@"2119716 Aethoro - Snowy.osz",
@"2120379 Synthion - VIVIDVELOCITY.osz",
@"2124805 Frums (unknown ""lambda"") - 19ZZ.osz",
@"2127811 Wiklund - Joy of Living (Cut Ver.).osz",
}; };
} }
} }

View File

@ -52,8 +52,11 @@ namespace osu.Game.Beatmaps.Drawables
private Drawable getDrawableForModel(IBeatmapInfo? model) private Drawable getDrawableForModel(IBeatmapInfo? model)
{ {
if (model == null)
return Empty();
// prefer online cover where available. // prefer online cover where available.
if (model?.BeatmapSet is IBeatmapSetOnlineInfo online) if (model.BeatmapSet is IBeatmapSetOnlineInfo online)
return new OnlineBeatmapSetCover(online, beatmapSetCoverType); return new OnlineBeatmapSetCover(online, beatmapSetCoverType);
if (model is BeatmapInfo localModel) if (model is BeatmapInfo localModel)

View File

@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => new Beatmap(); protected override IBeatmap GetBeatmap() => new Beatmap();
public override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); public override Texture GetBackground() => textures?.Get(@"Backgrounds/bg2");
protected override Track GetBeatmapTrack() => GetVirtualTrack(); protected override Track GetBeatmapTrack() => GetVirtualTrack();

View File

@ -167,8 +167,6 @@ namespace osu.Game.Beatmaps.Formats
beatmapInfo.SamplesMatchPlaybackRate = false; beatmapInfo.SamplesMatchPlaybackRate = false;
} }
protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(' ') || line.StartsWith('_');
protected override void ParseLine(Beatmap beatmap, Section section, string line) protected override void ParseLine(Beatmap beatmap, Section section, string line)
{ {
switch (section) switch (section)
@ -417,9 +415,12 @@ namespace osu.Game.Beatmaps.Formats
{ {
string[] split = line.Split(','); string[] split = line.Split(',');
if (!Enum.TryParse(split[0], out LegacyEventType type)) // Until we have full storyboard encoder coverage, let's track any lines which aren't handled
throw new InvalidDataException($@"Unknown event type: {split[0]}"); // and store them to a temporary location such that they aren't lost on editor save / export.
bool lineSupportedByEncoder = false;
if (Enum.TryParse(split[0], out LegacyEventType type))
{
switch (type) switch (type)
{ {
case LegacyEventType.Sprite: case LegacyEventType.Sprite:
@ -427,7 +428,11 @@ namespace osu.Game.Beatmaps.Formats
// In some older beatmaps, it is not present and replaced by a storyboard-level background instead. // In some older beatmaps, it is not present and replaced by a storyboard-level background instead.
// Allow the first sprite (by file order) to act as the background in such cases. // Allow the first sprite (by file order) to act as the background in such cases.
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile)) if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
lineSupportedByEncoder = true;
}
break; break;
case LegacyEventType.Video: case LegacyEventType.Video:
@ -439,12 +444,14 @@ namespace osu.Game.Beatmaps.Formats
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant())) if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{ {
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
lineSupportedByEncoder = true;
} }
break; break;
case LegacyEventType.Background: case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
lineSupportedByEncoder = true;
break; break;
case LegacyEventType.Break: case LegacyEventType.Break:
@ -452,10 +459,15 @@ namespace osu.Game.Beatmaps.Formats
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))); double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
beatmap.Breaks.Add(new BreakPeriod(start, end)); beatmap.Breaks.Add(new BreakPeriod(start, end));
lineSupportedByEncoder = true;
break; break;
} }
} }
if (!lineSupportedByEncoder)
beatmap.UnhandledEventLines.Add(line);
}
private void handleTimingPoint(string line) private void handleTimingPoint(string line)
{ {
string[] split = line.Split(','); string[] split = line.Split(',');

View File

@ -156,6 +156,9 @@ namespace osu.Game.Beatmaps.Formats
foreach (var b in beatmap.Breaks) foreach (var b in beatmap.Breaks)
writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}"));
foreach (string l in beatmap.UnhandledEventLines)
writer.WriteLine(l);
} }
private void handleControlPoints(TextWriter writer) private void handleControlPoints(TextWriter writer)

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Storyboards.Commands;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -17,7 +18,7 @@ namespace osu.Game.Beatmaps.Formats
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard> public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
{ {
private StoryboardSprite? storyboardSprite; private StoryboardSprite? storyboardSprite;
private CommandTimelineGroup? timelineGroup; private StoryboardCommandGroup? currentCommandsGroup;
private Storyboard storyboard = null!; private Storyboard storyboard = null!;
@ -114,7 +115,7 @@ namespace osu.Game.Beatmaps.Formats
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant())) if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
break; break;
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset)); storyboard.GetLayer("Video").Add(storyboardSprite = new StoryboardVideo(path, offset));
break; break;
} }
@ -164,7 +165,7 @@ namespace osu.Game.Beatmaps.Formats
else else
{ {
if (depth < 2) if (depth < 2)
timelineGroup = storyboardSprite?.TimelineGroup; currentCommandsGroup = storyboardSprite?.Commands;
string commandType = split[0]; string commandType = split[0];
@ -176,7 +177,7 @@ namespace osu.Game.Beatmaps.Formats
double startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue; double startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue;
double endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue; double endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue;
int groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0; int groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0;
timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); currentCommandsGroup = storyboardSprite?.AddTriggerGroup(triggerName, startTime, endTime, groupNumber);
break; break;
} }
@ -184,7 +185,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
double startTime = Parsing.ParseDouble(split[1]); double startTime = Parsing.ParseDouble(split[1]);
int repeatCount = Parsing.ParseInt(split[2]); int repeatCount = Parsing.ParseInt(split[2]);
timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1)); currentCommandsGroup = storyboardSprite?.AddLoopingGroup(startTime, Math.Max(0, repeatCount - 1));
break; break;
} }
@ -203,7 +204,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
float startValue = Parsing.ParseFloat(split[4]); float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); currentCommandsGroup?.AddAlpha(easing, startTime, endTime, startValue, endValue);
break; break;
} }
@ -211,7 +212,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
float startValue = Parsing.ParseFloat(split[4]); float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Scale.Add(easing, startTime, endTime, startValue, endValue); currentCommandsGroup?.AddScale(easing, startTime, endTime, startValue, endValue);
break; break;
} }
@ -221,7 +222,7 @@ namespace osu.Game.Beatmaps.Formats
float startY = Parsing.ParseFloat(split[5]); float startY = Parsing.ParseFloat(split[5]);
float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX;
float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY;
timelineGroup?.VectorScale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); currentCommandsGroup?.AddVectorScale(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY));
break; break;
} }
@ -229,7 +230,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
float startValue = Parsing.ParseFloat(split[4]); float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Rotation.Add(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue)); currentCommandsGroup?.AddRotation(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue));
break; break;
} }
@ -239,8 +240,8 @@ namespace osu.Game.Beatmaps.Formats
float startY = Parsing.ParseFloat(split[5]); float startY = Parsing.ParseFloat(split[5]);
float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX; float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX;
float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY; float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY;
timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); currentCommandsGroup?.AddX(easing, startTime, endTime, startX, endX);
timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); currentCommandsGroup?.AddY(easing, startTime, endTime, startY, endY);
break; break;
} }
@ -248,7 +249,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
float startValue = Parsing.ParseFloat(split[4]); float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); currentCommandsGroup?.AddX(easing, startTime, endTime, startValue, endValue);
break; break;
} }
@ -256,7 +257,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
float startValue = Parsing.ParseFloat(split[4]); float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue; float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); currentCommandsGroup?.AddY(easing, startTime, endTime, startValue, endValue);
break; break;
} }
@ -268,7 +269,7 @@ namespace osu.Game.Beatmaps.Formats
float endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed; float endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed;
float endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen; float endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen;
float endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue; float endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue;
timelineGroup?.Colour.Add(easing, startTime, endTime, currentCommandsGroup?.AddColour(easing, startTime, endTime,
new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1),
new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1));
break; break;
@ -281,16 +282,16 @@ namespace osu.Game.Beatmaps.Formats
switch (type) switch (type)
{ {
case "A": case "A":
timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, currentCommandsGroup?.AddBlendingParameters(easing, startTime, endTime, BlendingParameters.Additive,
startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
break; break;
case "H": case "H":
timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime); currentCommandsGroup?.AddFlipH(easing, startTime, endTime, true, startTime == endTime);
break; break;
case "V": case "V":
timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime); currentCommandsGroup?.AddFlipV(easing, startTime, endTime, true, startTime == endTime);
break; break;
} }

View File

@ -42,6 +42,12 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
List<BreakPeriod> Breaks { get; } List<BreakPeriod> Breaks { get; }
/// <summary>
/// All lines from the [Events] section which aren't handled in the encoding process yet.
/// These lines shoule be written out to the beatmap file on save or export.
/// </summary>
List<string> UnhandledEventLines { get; }
/// <summary> /// <summary>
/// Total amount of break time in the beatmap. /// Total amount of break time in the beatmap.
/// </summary> /// </summary>

View File

@ -44,11 +44,34 @@ namespace osu.Game.Beatmaps
this.storage = storage; this.storage = storage;
// avoid downloading / using cache for unit tests. if (shouldFetchCache())
if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name))
prepareLocalCache(); prepareLocalCache();
} }
private bool shouldFetchCache()
{
// avoid downloading / using cache for unit tests.
if (DebugUtils.IsNUnitRunning)
return false;
if (!storage.Exists(cache_database_name))
{
log(@"Fetching local cache because it does not exist.");
return true;
}
// periodically update the cache to include newer beatmaps.
var fileInfo = new FileInfo(storage.GetFullPath(cache_database_name));
if (fileInfo.LastWriteTime < DateTime.Now.AddMonths(-1))
{
log($@"Refetching local cache because it was last written to on {fileInfo.LastWriteTime}.");
return true;
}
return false;
}
public bool Available => public bool Available =>
// no download in progress. // no download in progress.
cacheDownloadRequest == null cacheDownloadRequest == null
@ -124,6 +147,8 @@ namespace osu.Game.Beatmaps
private void prepareLocalCache() private void prepareLocalCache()
{ {
bool isRefetch = storage.Exists(cache_database_name);
string cacheFilePath = storage.GetFullPath(cache_database_name); string cacheFilePath = storage.GetFullPath(cache_database_name);
string compressedCacheFilePath = $@"{cacheFilePath}.bz2"; string compressedCacheFilePath = $@"{cacheFilePath}.bz2";
@ -132,9 +157,15 @@ namespace osu.Game.Beatmaps
cacheDownloadRequest.Failed += ex => cacheDownloadRequest.Failed += ex =>
{ {
File.Delete(compressedCacheFilePath); File.Delete(compressedCacheFilePath);
// don't clobber the cache when refetching if the download didn't succeed. seems excessive.
// consequently, also null the download request to allow the existing cache to be used (see `Available`).
if (isRefetch)
cacheDownloadRequest = null;
else
File.Delete(cacheFilePath); File.Delete(cacheFilePath);
Logger.Log($@"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache download failed: {ex}", LoggingTarget.Database); log($@"Online cache download failed: {ex}");
}; };
cacheDownloadRequest.Finished += () => cacheDownloadRequest.Finished += () =>
@ -143,15 +174,22 @@ namespace osu.Game.Beatmaps
{ {
using (var stream = File.OpenRead(cacheDownloadRequest.Filename)) using (var stream = File.OpenRead(cacheDownloadRequest.Filename))
using (var outStream = File.OpenWrite(cacheFilePath)) using (var outStream = File.OpenWrite(cacheFilePath))
{
// ensure to clobber any and all existing data to avoid accidental corruption.
outStream.SetLength(0);
using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false)) using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
bz2.CopyTo(outStream); bz2.CopyTo(outStream);
}
// set to null on completion to allow lookups to begin using the new source // set to null on completion to allow lookups to begin using the new source
cacheDownloadRequest = null; cacheDownloadRequest = null;
log(@"Local cache fetch completed successfully.");
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Log($@"{nameof(LocalCachedBeatmapMetadataSource)}'s online cache extraction failed: {ex}", LoggingTarget.Database); log($@"Online cache extraction failed: {ex}");
// at this point clobber the cache regardless of whether we're refetching, because by this point who knows what state the cache file is in.
File.Delete(cacheFilePath); File.Delete(cacheFilePath);
} }
finally finally
@ -173,6 +211,9 @@ namespace osu.Game.Beatmaps
}); });
} }
private static void log(string message)
=> Logger.Log($@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}", LoggingTarget.Database);
private void logForModel(BeatmapSetInfo set, string message) => private void logForModel(BeatmapSetInfo set, string message) =>
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}"); RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}");

View File

@ -291,6 +291,8 @@ namespace osu.Game.Database
{ {
var score = scoreManager.Query(s => s.ID == id); var score = scoreManager.Query(s => s.ID == id);
if (score != null)
{
scoreManager.PopulateMaximumStatistics(score); scoreManager.PopulateMaximumStatistics(score);
// Can't use async overload because we're not on the update thread. // Can't use async overload because we're not on the update thread.
@ -299,6 +301,7 @@ namespace osu.Game.Database
{ {
r.Find<ScoreInfo>(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics); r.Find<ScoreInfo>(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
}); });
}
++processedCount; ++processedCount;
} }

View File

@ -17,6 +17,8 @@ namespace osu.Game.Database
{ {
} }
protected override bool UseFixedEncoding => false;
protected override string FileExtension => @".olz"; protected override string FileExtension => @".olz";
} }
} }

View File

@ -3,10 +3,12 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using Realms; using Realms;
using SharpCompress.Common; using SharpCompress.Common;
@ -22,6 +24,11 @@ namespace osu.Game.Database
public abstract class LegacyArchiveExporter<TModel> : LegacyExporter<TModel> public abstract class LegacyArchiveExporter<TModel> : LegacyExporter<TModel>
where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey where TModel : RealmObject, IHasNamedFiles, IHasGuidPrimaryKey
{ {
/// <summary>
/// Whether to always use Shift-JIS encoding for archive filenames (like osu!stable did).
/// </summary>
protected virtual bool UseFixedEncoding => true;
protected LegacyArchiveExporter(Storage storage) protected LegacyArchiveExporter(Storage storage)
: base(storage) : base(storage)
{ {
@ -29,7 +36,12 @@ namespace osu.Game.Database
public override void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default) public override void ExportToStream(TModel model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = default)
{ {
using (var writer = new ZipWriter(outputStream, new ZipWriterOptions(CompressionType.Deflate))) var zipWriterOptions = new ZipWriterOptions(CompressionType.Deflate)
{
ArchiveEncoding = UseFixedEncoding ? ZipArchiveReader.DEFAULT_ENCODING : new ArchiveEncoding(Encoding.UTF8, Encoding.UTF8)
};
using (var writer = new ZipWriter(outputStream, zipWriterOptions))
{ {
int i = 0; int i = 0;
int fileCount = model.Files.Count(); int fileCount = model.Files.Count();

View File

@ -35,6 +35,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy; using osu.Game.Scoring.Legacy;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Input; using osuTK.Input;
using Realms; using Realms;
using Realms.Exceptions; using Realms.Exceptions;
@ -322,12 +323,32 @@ namespace osu.Game.Database
{ {
Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data."); Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data.");
// If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about. // If a newer version database already exists, don't create another backup. We can presume that the first backup is the one we care about.
if (!storage.Exists(newerVersionFilename)) if (!storage.Exists(newerVersionFilename))
createBackup(newerVersionFilename); createBackup(newerVersionFilename);
} }
else else
{ {
// This error can occur due to file handles still being open by a previous instance.
// If this is the case, rather than assuming the realm file is corrupt, block game startup.
if (e.Message.StartsWith("SetEndOfFile() failed", StringComparison.Ordinal))
{
// This will throw if the realm file is not available for write access after 5 seconds.
FileUtils.AttemptOperation(() =>
{
if (storage.Exists(Filename))
{
using (var _ = storage.GetStream(Filename, FileAccess.ReadWrite))
{
}
}
}, 20);
// If the above eventually succeeds, try and continue startup as per normal.
// This may throw again but let's allow it to, and block startup.
return getRealmInstance();
}
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made."); Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}"); createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
} }
@ -1149,11 +1170,7 @@ namespace osu.Game.Database
{ {
Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database); Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database);
int attempts = 10; FileUtils.AttemptOperation(() =>
while (true)
{
try
{ {
using (var source = storage.GetStream(Filename, mode: FileMode.Open)) using (var source = storage.GetStream(Filename, mode: FileMode.Open))
{ {
@ -1164,18 +1181,7 @@ namespace osu.Game.Database
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew)) using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
source.CopyTo(destination); source.CopyTo(destination);
} }
}, 20);
return;
}
catch (IOException)
{
if (attempts-- <= 0)
throw;
// file may be locked during use.
Thread.Sleep(500);
}
}
} }
/// <summary> /// <summary>

View File

@ -449,16 +449,6 @@ namespace osu.Game.Database
return reader.Name.ComputeSHA2Hash(); return reader.Name.ComputeSHA2Hash();
} }
/// <summary>
/// Create all required <see cref="File"/>s for the provided archive, adding them to the global file store.
/// </summary>
private List<RealmNamedFileUsage> createFileInfos(ArchiveReader reader, RealmFileStore files, Realm realm)
{
var fileInfos = new List<RealmNamedFileUsage>();
return fileInfos;
}
private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader) private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader)
{ {
string prefix = reader.Filenames.GetCommonPrefix(); string prefix = reader.Filenames.GetCommonPrefix();

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -12,6 +13,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
@ -40,6 +42,27 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(hoverClickSounds = new HoverClickSounds()); AddInternal(hoverClickSounds = new HoverClickSounds());
updateTextColour(); updateTextColour();
bool hasSubmenu = Item.Items.Any();
// Only add right chevron if direction of menu items is vertical (i.e. width is relative size, see `DrawableMenuItem.SetFlowDirection()`).
if (hasSubmenu && RelativeSizeAxes == Axes.X)
{
AddInternal(new SpriteIcon
{
Margin = new MarginPadding(6),
Size = new Vector2(8),
Icon = FontAwesome.Solid.ChevronRight,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
});
text.Padding = new MarginPadding
{
// Add some padding for the chevron above.
Right = 5,
};
}
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -4,7 +4,9 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -19,6 +21,16 @@ namespace osu.Game.Graphics.UserInterface
protected override TextContainer CreateTextContainer() => new ToggleTextContainer(Item); protected override TextContainer CreateTextContainer() => new ToggleTextContainer(Item);
protected override bool OnMouseDown(MouseDownEvent e)
{
// Right mouse button is a special case where we allow actioning without dismissing the menu.
// This is achieved by not calling `Clicked` (as done by the base implementation in OnClick).
if (IsActionable && e.Button == MouseButton.Right)
Item.Action.Value?.Invoke();
return true;
}
private partial class ToggleTextContainer : TextContainer private partial class ToggleTextContainer : TextContainer
{ {
private readonly StatefulMenuItem menuItem; private readonly StatefulMenuItem menuItem;

View File

@ -1,7 +1,7 @@
// 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.Numerics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -18,7 +18,7 @@ namespace osu.Game.Graphics.UserInterface
/// An <see cref="IExpandable"/> implementation for the UI slider bar control. /// An <see cref="IExpandable"/> implementation for the UI slider bar control.
/// </summary> /// </summary>
public partial class ExpandableSlider<T, TSlider> : CompositeDrawable, IExpandable, IHasCurrentValue<T> public partial class ExpandableSlider<T, TSlider> : CompositeDrawable, IExpandable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
where TSlider : RoundedSliderBar<T>, new() where TSlider : RoundedSliderBar<T>, new()
{ {
private readonly OsuSpriteText label; private readonly OsuSpriteText label;
@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface
/// An <see cref="IExpandable"/> implementation for the UI slider bar control. /// An <see cref="IExpandable"/> implementation for the UI slider bar control.
/// </summary> /// </summary>
public partial class ExpandableSlider<T> : ExpandableSlider<T, RoundedSliderBar<T>> public partial class ExpandableSlider<T> : ExpandableSlider<T, RoundedSliderBar<T>>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
{ {
} }
} }

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.Numerics;
using System.Globalization; using System.Globalization;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -15,7 +16,7 @@ using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public abstract partial class OsuSliderBar<T> : SliderBar<T>, IHasTooltip public abstract partial class OsuSliderBar<T> : SliderBar<T>, IHasTooltip
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
{ {
public bool PlaySamplesOnAdjust { get; set; } = true; public bool PlaySamplesOnAdjust { get; set; } = true;
@ -85,11 +86,11 @@ namespace osu.Game.Graphics.UserInterface
private LocalisableString getTooltipText(T value) private LocalisableString getTooltipText(T value)
{ {
if (CurrentNumber.IsInteger) if (CurrentNumber.IsInteger)
return value.ToInt32(NumberFormatInfo.InvariantInfo).ToString("N0"); return int.CreateTruncating(value).ToString("N0");
double floatValue = value.ToDouble(NumberFormatInfo.InvariantInfo); double floatValue = double.CreateTruncating(value);
decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits); decimal decimalPrecision = normalise(decimal.CreateTruncating(CurrentNumber.Precision), max_decimal_digits);
// Find the number of significant digits (we could have less than 5 after normalize()) // Find the number of significant digits (we could have less than 5 after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision); int significantDigits = FormatUtils.FindPrecision(decimalPrecision);

View File

@ -8,6 +8,8 @@ using System.Linq;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -143,13 +145,6 @@ namespace osu.Game.Graphics.UserInterface
FadeUnhovered(); FadeUnhovered();
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (accentColour == default)
AccentColour = colours.Blue;
}
public OsuTabItem(T value) public OsuTabItem(T value)
: base(value) : base(value)
{ {
@ -196,10 +191,21 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
}, },
new HoverClickSounds(HoverSampleSet.TabSelect) new HoverSounds(HoverSampleSet.TabSelect)
}; };
} }
private Sample selectSample;
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
{
if (accentColour == default)
AccentColour = colours.Blue;
selectSample = audio.Samples.Get(@"UI/tabselect-select");
}
protected override void OnActivated() protected override void OnActivated()
{ {
Text.Font = Text.Font.With(weight: FontWeight.Bold); Text.Font = Text.Font.With(weight: FontWeight.Bold);
@ -211,6 +217,8 @@ namespace osu.Game.Graphics.UserInterface
Text.Font = Text.Font.With(weight: FontWeight.Medium); Text.Font = Text.Font.With(weight: FontWeight.Medium);
FadeUnhovered(); FadeUnhovered();
} }
protected override void OnActivatedByUser() => selectSample.Play();
} }
} }
} }

View File

@ -7,6 +7,8 @@ using System;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -53,6 +55,8 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
private Sample selectSample = null!;
public PageTabItem(T value) public PageTabItem(T value)
: base(value) : base(value)
{ {
@ -78,12 +82,18 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
}, },
new HoverClickSounds(HoverSampleSet.TabSelect) new HoverSounds(HoverSampleSet.TabSelect)
}; };
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
} }
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
selectSample = audio.Samples.Get(@"UI/tabselect-select");
}
protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString(); protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString();
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
@ -112,6 +122,8 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnActivated() => slideActive(); protected override void OnActivated() => slideActive();
protected override void OnDeactivated() => slideInactive(); protected override void OnDeactivated() => slideInactive();
protected override void OnActivatedByUser() => selectSample.Play();
} }
} }
} }

View File

@ -2,7 +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 osuTK; using System.Numerics;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -11,11 +11,12 @@ 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.Game.Overlays; using osu.Game.Overlays;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public partial class RoundedSliderBar<T> : OsuSliderBar<T> public partial class RoundedSliderBar<T> : OsuSliderBar<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
{ {
protected readonly Nub Nub; protected readonly Nub Nub;
protected readonly Box LeftBox; protected readonly Box LeftBox;

View File

@ -2,7 +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 osuTK; using System.Numerics;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -12,11 +12,12 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Overlays; using osu.Game.Overlays;
using static osu.Game.Graphics.UserInterface.ShearedNub; using static osu.Game.Graphics.UserInterface.ShearedNub;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public partial class ShearedSliderBar<T> : OsuSliderBar<T> public partial class ShearedSliderBar<T> : OsuSliderBar<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
{ {
protected readonly ShearedNub Nub; protected readonly ShearedNub Nub;
protected readonly Box LeftBox; protected readonly Box LeftBox;

View File

@ -1,14 +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.Numerics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public partial class LabelledSliderBar<TNumber> : LabelledComponent<SettingsSlider<TNumber>, TNumber> public partial class LabelledSliderBar<TNumber> : LabelledComponent<SettingsSlider<TNumber>, TNumber>
where TNumber : struct, IEquatable<TNumber>, IComparable<TNumber>, IConvertible where TNumber : struct, INumber<TNumber>, IMinMaxValue<TNumber>
{ {
public LabelledSliderBar() public LabelledSliderBar()
: base(true) : base(true)

View File

@ -1,7 +1,7 @@
// 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.Numerics;
using System.Globalization; using System.Globalization;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,12 +10,12 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T> public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
{ {
/// <summary> /// <summary>
/// A custom step value for each key press which actuates a change on this control. /// A custom step value for each key press which actuates a change on this control.
@ -138,7 +138,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
if (updatingFromTextBox) return; if (updatingFromTextBox) return;
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo); decimal decimalValue = decimal.CreateTruncating(slider.Current.Value);
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}"); textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
} }
} }

View File

@ -7,23 +7,45 @@ using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using Microsoft.Toolkit.HighPerformance; using Microsoft.Toolkit.HighPerformance;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Readers;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
namespace osu.Game.IO.Archives namespace osu.Game.IO.Archives
{ {
public sealed class ZipArchiveReader : ArchiveReader public sealed class ZipArchiveReader : ArchiveReader
{ {
/// <summary>
/// Archives created by osu!stable still write out as Shift-JIS.
/// We want to force this fallback rather than leave it up to the library/system.
/// In the future we may want to change exports to set the zip UTF-8 flag and use that instead.
/// </summary>
public static readonly ArchiveEncoding DEFAULT_ENCODING;
private readonly Stream archiveStream; private readonly Stream archiveStream;
private readonly ZipArchive archive; private readonly ZipArchive archive;
static ZipArchiveReader()
{
// Required to support rare code pages.
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
DEFAULT_ENCODING = new ArchiveEncoding(Encoding.GetEncoding(932), Encoding.GetEncoding(932));
}
public ZipArchiveReader(Stream archiveStream, string name = null) public ZipArchiveReader(Stream archiveStream, string name = null)
: base(name) : base(name)
{ {
this.archiveStream = archiveStream; this.archiveStream = archiveStream;
archive = ZipArchive.Open(archiveStream);
archive = ZipArchive.Open(archiveStream, new ReaderOptions
{
ArchiveEncoding = DEFAULT_ENCODING
});
} }
public override Stream GetStream(string name) public override Stream GetStream(string name)

View File

@ -123,58 +123,58 @@ namespace osu.Game.IO.Legacy
switch (t) switch (t)
{ {
case ObjType.boolType: case ObjType.BoolType:
return ReadBoolean(); return ReadBoolean();
case ObjType.byteType: case ObjType.ByteType:
return ReadByte(); return ReadByte();
case ObjType.uint16Type: case ObjType.UInt16Type:
return ReadUInt16(); return ReadUInt16();
case ObjType.uint32Type: case ObjType.UInt32Type:
return ReadUInt32(); return ReadUInt32();
case ObjType.uint64Type: case ObjType.UInt64Type:
return ReadUInt64(); return ReadUInt64();
case ObjType.sbyteType: case ObjType.SByteType:
return ReadSByte(); return ReadSByte();
case ObjType.int16Type: case ObjType.Int16Type:
return ReadInt16(); return ReadInt16();
case ObjType.int32Type: case ObjType.Int32Type:
return ReadInt32(); return ReadInt32();
case ObjType.int64Type: case ObjType.Int64Type:
return ReadInt64(); return ReadInt64();
case ObjType.charType: case ObjType.CharType:
return ReadChar(); return ReadChar();
case ObjType.stringType: case ObjType.StringType:
return base.ReadString(); return base.ReadString();
case ObjType.singleType: case ObjType.SingleType:
return ReadSingle(); return ReadSingle();
case ObjType.doubleType: case ObjType.DoubleType:
return ReadDouble(); return ReadDouble();
case ObjType.decimalType: case ObjType.DecimalType:
return ReadDecimal(); return ReadDecimal();
case ObjType.dateTimeType: case ObjType.DateTimeType:
return ReadDateTime(); return ReadDateTime();
case ObjType.byteArrayType: case ObjType.ByteArrayType:
return ReadByteArray(); return ReadByteArray();
case ObjType.charArrayType: case ObjType.CharArrayType:
return ReadCharArray(); return ReadCharArray();
case ObjType.otherType: case ObjType.OtherType:
throw new IOException("Deserialization of arbitrary type is not supported."); throw new IOException("Deserialization of arbitrary type is not supported.");
default: default:
@ -185,25 +185,25 @@ namespace osu.Game.IO.Legacy
public enum ObjType : byte public enum ObjType : byte
{ {
nullType, NullType,
boolType, BoolType,
byteType, ByteType,
uint16Type, UInt16Type,
uint32Type, UInt32Type,
uint64Type, UInt64Type,
sbyteType, SByteType,
int16Type, Int16Type,
int32Type, Int32Type,
int64Type, Int64Type,
charType, CharType,
stringType, StringType,
singleType, SingleType,
doubleType, DoubleType,
decimalType, DecimalType,
dateTimeType, DateTimeType,
byteArrayType, ByteArrayType,
charArrayType, CharArrayType,
otherType, OtherType,
ILegacySerializableType LegacySerializableType
} }
} }

View File

@ -34,11 +34,11 @@ namespace osu.Game.IO.Legacy
{ {
if (str == null) if (str == null)
{ {
Write((byte)ObjType.nullType); Write((byte)ObjType.NullType);
} }
else else
{ {
Write((byte)ObjType.stringType); Write((byte)ObjType.StringType);
base.Write(str); base.Write(str);
} }
} }
@ -125,94 +125,94 @@ namespace osu.Game.IO.Legacy
{ {
if (obj == null) if (obj == null)
{ {
Write((byte)ObjType.nullType); Write((byte)ObjType.NullType);
} }
else else
{ {
switch (obj) switch (obj)
{ {
case bool boolObj: case bool boolObj:
Write((byte)ObjType.boolType); Write((byte)ObjType.BoolType);
Write(boolObj); Write(boolObj);
break; break;
case byte byteObj: case byte byteObj:
Write((byte)ObjType.byteType); Write((byte)ObjType.ByteType);
Write(byteObj); Write(byteObj);
break; break;
case ushort ushortObj: case ushort ushortObj:
Write((byte)ObjType.uint16Type); Write((byte)ObjType.UInt16Type);
Write(ushortObj); Write(ushortObj);
break; break;
case uint uintObj: case uint uintObj:
Write((byte)ObjType.uint32Type); Write((byte)ObjType.UInt32Type);
Write(uintObj); Write(uintObj);
break; break;
case ulong ulongObj: case ulong ulongObj:
Write((byte)ObjType.uint64Type); Write((byte)ObjType.UInt64Type);
Write(ulongObj); Write(ulongObj);
break; break;
case sbyte sbyteObj: case sbyte sbyteObj:
Write((byte)ObjType.sbyteType); Write((byte)ObjType.SByteType);
Write(sbyteObj); Write(sbyteObj);
break; break;
case short shortObj: case short shortObj:
Write((byte)ObjType.int16Type); Write((byte)ObjType.Int16Type);
Write(shortObj); Write(shortObj);
break; break;
case int intObj: case int intObj:
Write((byte)ObjType.int32Type); Write((byte)ObjType.Int32Type);
Write(intObj); Write(intObj);
break; break;
case long longObj: case long longObj:
Write((byte)ObjType.int64Type); Write((byte)ObjType.Int64Type);
Write(longObj); Write(longObj);
break; break;
case char charObj: case char charObj:
Write((byte)ObjType.charType); Write((byte)ObjType.CharType);
base.Write(charObj); base.Write(charObj);
break; break;
case string stringObj: case string stringObj:
Write((byte)ObjType.stringType); Write((byte)ObjType.StringType);
base.Write(stringObj); base.Write(stringObj);
break; break;
case float floatObj: case float floatObj:
Write((byte)ObjType.singleType); Write((byte)ObjType.SingleType);
Write(floatObj); Write(floatObj);
break; break;
case double doubleObj: case double doubleObj:
Write((byte)ObjType.doubleType); Write((byte)ObjType.DoubleType);
Write(doubleObj); Write(doubleObj);
break; break;
case decimal decimalObj: case decimal decimalObj:
Write((byte)ObjType.decimalType); Write((byte)ObjType.DecimalType);
Write(decimalObj); Write(decimalObj);
break; break;
case DateTime dateTimeObj: case DateTime dateTimeObj:
Write((byte)ObjType.dateTimeType); Write((byte)ObjType.DateTimeType);
Write(dateTimeObj); Write(dateTimeObj);
break; break;
case byte[] byteArray: case byte[] byteArray:
Write((byte)ObjType.byteArrayType); Write((byte)ObjType.ByteArrayType);
base.Write(byteArray); base.Write(byteArray);
break; break;
case char[] charArray: case char[] charArray:
Write((byte)ObjType.charArrayType); Write((byte)ObjType.CharArrayType);
base.Write(charArray); base.Write(charArray);
break; break;

View File

@ -42,7 +42,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "If enabled, an &quot;Are you ready? 3, 2, 1, GO!&quot; countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so." /// "If enabled, an &quot;Are you ready? 3, 2, 1, GO!&quot; countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."
/// </summary> /// </summary>
public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"), @"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so."); public static LocalisableString CountdownDescription => new TranslatableString(getKey(@"countdown_description"),
@"If enabled, an ""Are you ready? 3, 2, 1, GO!"" countdown will be inserted at the beginning of the beatmap, assuming there is enough time to do so.");
/// <summary> /// <summary>
/// "Countdown speed" /// "Countdown speed"
@ -52,7 +53,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "If the countdown sounds off-time, use this to make it appear one or more beats early." /// "If the countdown sounds off-time, use this to make it appear one or more beats early."
/// </summary> /// </summary>
public static LocalisableString CountdownOffsetDescription => new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early."); public static LocalisableString CountdownOffsetDescription =>
new TranslatableString(getKey(@"countdown_offset_description"), @"If the countdown sounds off-time, use this to make it appear one or more beats early.");
/// <summary> /// <summary>
/// "Countdown offset" /// "Countdown offset"
@ -67,7 +69,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area." /// "Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."
/// </summary> /// </summary>
public static LocalisableString WidescreenSupportDescription => new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area."); public static LocalisableString WidescreenSupportDescription =>
new TranslatableString(getKey(@"widescreen_support_description"), @"Allows storyboards to use the full screen space, rather than be confined to a 4:3 area.");
/// <summary> /// <summary>
/// "Epilepsy warning" /// "Epilepsy warning"
@ -77,7 +80,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "Recommended if the storyboard or video contain scenes with rapidly flashing colours." /// "Recommended if the storyboard or video contain scenes with rapidly flashing colours."
/// </summary> /// </summary>
public static LocalisableString EpilepsyWarningDescription => new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours."); public static LocalisableString EpilepsyWarningDescription =>
new TranslatableString(getKey(@"epilepsy_warning_description"), @"Recommended if the storyboard or video contain scenes with rapidly flashing colours.");
/// <summary> /// <summary>
/// "Letterbox during breaks" /// "Letterbox during breaks"
@ -87,7 +91,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "Adds horizontal letterboxing to give a cinematic look during breaks." /// "Adds horizontal letterboxing to give a cinematic look during breaks."
/// </summary> /// </summary>
public static LocalisableString LetterboxDuringBreaksDescription => new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks."); public static LocalisableString LetterboxDuringBreaksDescription =>
new TranslatableString(getKey(@"letterbox_during_breaks_description"), @"Adds horizontal letterboxing to give a cinematic look during breaks.");
/// <summary> /// <summary>
/// "Samples match playback rate" /// "Samples match playback rate"
@ -97,7 +102,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled." /// "When enabled, all samples will speed up or slow down when rate-changing mods are enabled."
/// </summary> /// </summary>
public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"), @"When enabled, all samples will speed up or slow down when rate-changing mods are enabled."); public static LocalisableString SamplesMatchPlaybackRateDescription => new TranslatableString(getKey(@"samples_match_playback_rate_description"),
@"When enabled, all samples will speed up or slow down when rate-changing mods are enabled.");
/// <summary> /// <summary>
/// "The size of all hit objects" /// "The size of all hit objects"
@ -117,7 +123,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "The harshness of hit windows and difficulty of special objects (ie. spinners)" /// "The harshness of hit windows and difficulty of special objects (ie. spinners)"
/// </summary> /// </summary>
public static LocalisableString OverallDifficultyDescription => new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)"); public static LocalisableString OverallDifficultyDescription =>
new TranslatableString(getKey(@"overall_difficulty_description"), @"The harshness of hit windows and difficulty of special objects (ie. spinners)");
/// <summary> /// <summary>
/// "Tick Rate" /// "Tick Rate"
@ -127,7 +134,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "Determines how many &quot;ticks&quot; are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc." /// "Determines how many &quot;ticks&quot; are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc."
/// </summary> /// </summary>
public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"), @"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc."); public static LocalisableString TickRateDescription => new TranslatableString(getKey(@"tick_rate_description"),
@"Determines how many ""ticks"" are generated within long hit objects. A tick rate of 1 will generate ticks on each beat, 2 would be twice per beat, etc.");
/// <summary> /// <summary>
/// "Base Velocity" /// "Base Velocity"
@ -137,7 +145,8 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets." /// "The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets."
/// </summary> /// </summary>
public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"), @"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets."); public static LocalisableString BaseVelocityDescription => new TranslatableString(getKey(@"base_velocity_description"),
@"The base velocity of the beatmap, affecting things like slider velocity and scroll speed in some rulesets.");
/// <summary> /// <summary>
/// "Metadata" /// "Metadata"
@ -159,6 +168,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Creator => new TranslatableString(getKey(@"creator"), @"Creator"); public static LocalisableString Creator => new TranslatableString(getKey(@"creator"), @"Creator");
/// <summary>
/// "Source"
/// </summary>
public static LocalisableString Source => new TranslatableString(getKey(@"source"), @"Source");
/// <summary> /// <summary>
/// "Difficulty Name" /// "Difficulty Name"
/// </summary> /// </summary>

View File

@ -39,6 +39,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString BundledButton => new TranslatableString(getKey(@"bundled_button"), @"Get recommended beatmaps"); public static LocalisableString BundledButton => new TranslatableString(getKey(@"bundled_button"), @"Get recommended beatmaps");
/// <summary>
/// "Beatmaps will be downloaded in the background. You can continue with setup while this happens!"
/// </summary>
public static LocalisableString DownloadingInBackground => new TranslatableString(getKey(@"downloading_in_background"), @"Beatmaps will be downloaded in the background. You can continue with setup while this happens!");
/// <summary> /// <summary>
/// "You can also obtain more beatmaps from the main menu &quot;browse&quot; button at any time." /// "You can also obtain more beatmaps from the main menu &quot;browse&quot; button at any time."
/// </summary> /// </summary>

View File

@ -2,10 +2,12 @@
// 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 System.Diagnostics.CodeAnalysis;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace osu.Game.Localisation namespace osu.Game.Localisation
{ {
[SuppressMessage("ReSharper", "InconsistentNaming")]
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public enum Language public enum Language
{ {

View File

@ -396,7 +396,7 @@ namespace osu.Game.Online.Multiplayer
switch (state) switch (state)
{ {
case MultiplayerRoomState.Open: case MultiplayerRoomState.Open:
APIRoom.Status.Value = new RoomStatusOpen(); APIRoom.Status.Value = APIRoom.HasPassword.Value ? new RoomStatusOpenPrivate() : new RoomStatusOpen();
break; break;
case MultiplayerRoomState.Playing: case MultiplayerRoomState.Playing:
@ -816,6 +816,7 @@ namespace osu.Game.Online.Multiplayer
Room.Settings = settings; Room.Settings = settings;
APIRoom.Name.Value = Room.Settings.Name; APIRoom.Name.Value = Room.Settings.Name;
APIRoom.Password.Value = Room.Settings.Password; APIRoom.Password.Value = Room.Settings.Password;
APIRoom.Status.Value = string.IsNullOrEmpty(Room.Settings.Password) ? new RoomStatusOpen() : new RoomStatusOpenPrivate();
APIRoom.Type.Value = Room.Settings.MatchType; APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode; APIRoom.QueueMode.Value = Room.Settings.QueueMode;
APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration; APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration;

View File

@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Online.Rooms namespace osu.Game.Online.Rooms
@ -33,6 +35,25 @@ namespace osu.Game.Online.Rooms
return req; return req;
} }
protected override void PostProcess()
{
base.PostProcess();
if (Response != null)
{
// API doesn't populate status so let's do it here.
foreach (var room in Response)
{
if (room.EndDate.Value != null && DateTimeOffset.Now >= room.EndDate.Value)
room.Status.Value = new RoomStatusEnded();
else if (room.HasPassword.Value)
room.Status.Value = new RoomStatusOpenPrivate();
else
room.Status.Value = new RoomStatusOpen();
}
}
}
protected override string Target => "rooms"; protected override string Target => "rooms";
} }
} }

View File

@ -112,7 +112,7 @@ namespace osu.Game.Online.Rooms
public readonly Bindable<PlaylistAggregateScore> UserScore = new Bindable<PlaylistAggregateScore>(); public readonly Bindable<PlaylistAggregateScore> UserScore = new Bindable<PlaylistAggregateScore>();
[JsonProperty("has_password")] [JsonProperty("has_password")]
public readonly BindableBool HasPassword = new BindableBool(); public readonly Bindable<bool> HasPassword = new Bindable<bool>();
[Cached] [Cached]
[JsonProperty("recent_participants")] [JsonProperty("recent_participants")]
@ -201,9 +201,6 @@ namespace osu.Game.Online.Rooms
CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value; CurrentPlaylistItem.Value = other.CurrentPlaylistItem.Value;
AutoSkip.Value = other.AutoSkip.Value; AutoSkip.Value = other.AutoSkip.Value;
if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value)
Status.Value = new RoomStatusEnded();
other.RemoveExpiredPlaylistItems(); other.RemoveExpiredPlaylistItems();
if (!Playlist.SequenceEqual(other.Playlist)) if (!Playlist.SequenceEqual(other.Playlist))

View File

@ -0,0 +1,14 @@
// 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.Graphics;
using osuTK.Graphics;
namespace osu.Game.Online.Rooms.RoomStatuses
{
public class RoomStatusOpenPrivate : RoomStatus
{
public override string Message => "Open (Private)";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDark;
}
}

View File

@ -706,26 +706,9 @@ namespace osu.Game
{ {
Logger.Log($"Beginning {nameof(PresentScore)} with score {score}"); Logger.Log($"Beginning {nameof(PresentScore)} with score {score}");
// The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database var databasedScore = ScoreManager.GetScore(score);
// to ensure all the required data for presenting a replay are present.
ScoreInfo databasedScoreInfo = null;
if (score.OnlineID > 0) if (databasedScore == null) return;
databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID);
if (score.LegacyOnlineID > 0)
databasedScoreInfo ??= ScoreManager.Query(s => s.LegacyOnlineID == score.LegacyOnlineID);
if (score is ScoreInfo scoreInfo)
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash);
if (databasedScoreInfo == null)
{
Logger.Log("The requested score could not be found locally.", LoggingTarget.Information);
return;
}
var databasedScore = ScoreManager.GetScore(databasedScoreInfo);
if (databasedScore.Replay == null) if (databasedScore.Replay == null)
{ {
@ -733,7 +716,7 @@ namespace osu.Game
return; return;
} }
var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScoreInfo.BeatmapInfo.ID); var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScore.ScoreInfo.BeatmapInfo.ID);
if (databasedBeatmap == null) if (databasedBeatmap == null)
{ {
@ -858,7 +841,10 @@ namespace osu.Game
{ {
// General expectation that osu! starts in fullscreen by default (also gives the most predictable performance). // General expectation that osu! starts in fullscreen by default (also gives the most predictable performance).
// However, macOS is bound to have issues when using exclusive fullscreen as it takes full control away from OS, therefore borderless is default there. // However, macOS is bound to have issues when using exclusive fullscreen as it takes full control away from OS, therefore borderless is default there.
{ FrameworkSetting.WindowMode, RuntimeInfo.OS == RuntimeInfo.Platform.macOS ? WindowMode.Borderless : WindowMode.Fullscreen } { FrameworkSetting.WindowMode, RuntimeInfo.OS == RuntimeInfo.Platform.macOS ? WindowMode.Borderless : WindowMode.Fullscreen },
{ FrameworkSetting.VolumeUniversal, 0.6 },
{ FrameworkSetting.VolumeMusic, 0.6 },
{ FrameworkSetting.VolumeEffect, 0.6 },
}; };
} }

View File

@ -94,7 +94,7 @@ namespace osu.Game
public const int SAMPLE_DEBOUNCE_TIME = 20; public const int SAMPLE_DEBOUNCE_TIME = 20;
/// <summary> /// <summary>
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects. /// The maximum volume at which audio tracks should play back at. This can be set lower than 1 to create some head-room for sound effects.
/// </summary> /// </summary>
private const double global_track_volume_adjust = 0.8; private const double global_track_volume_adjust = 0.8;

View File

@ -5,6 +5,8 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
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;
@ -47,13 +49,15 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } private OverlayColourProvider colourProvider { get; set; }
private Sample selectSample = null!;
public TabItem(BeatmapCardSize value) public TabItem(BeatmapCardSize value)
: base(value) : base(value)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Masking = true; Masking = true;
@ -79,8 +83,10 @@ namespace osu.Game.Overlays.BeatmapListing
Icon = getIconForCardSize(Value) Icon = getIconForCardSize(Value)
} }
}, },
new HoverClickSounds(HoverSampleSet.TabSelect) new HoverSounds(HoverSampleSet.TabSelect)
}; };
selectSample = audio.Samples.Get(@"UI/tabselect-select");
} }
private static IconUsage getIconForCardSize(BeatmapCardSize cardSize) private static IconUsage getIconForCardSize(BeatmapCardSize cardSize)
@ -111,6 +117,8 @@ namespace osu.Game.Overlays.BeatmapListing
updateState(); updateState();
} }
protected override void OnActivatedByUser() => selectSample.Play();
protected override void OnDeactivated() protected override void OnDeactivated()
{ {
if (IsLoaded) if (IsLoaded)

View File

@ -128,7 +128,11 @@ namespace osu.Game.Overlays.BeatmapListing
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
base.OnClick(e); base.OnClick(e);
// this tab item implementation is not managed by a TabControl,
// therefore we have to manually update Active state and play select sound when this tab item is clicked.
Active.Toggle(); Active.Toggle();
SelectSample.Play();
return true; return true;
} }
} }

View File

@ -5,6 +5,8 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -24,13 +26,15 @@ namespace osu.Game.Overlays.BeatmapListing
private OsuSpriteText text; private OsuSpriteText text;
protected Sample SelectSample { get; private set; } = null!;
public FilterTabItem(T value) public FilterTabItem(T value)
: base(value) : base(value)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
@ -40,10 +44,12 @@ namespace osu.Game.Overlays.BeatmapListing
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
Text = LabelFor(Value) Text = LabelFor(Value)
}, },
new HoverClickSounds(HoverSampleSet.TabSelect) new HoverSounds(HoverSampleSet.TabSelect)
}); });
Enabled.Value = true; Enabled.Value = true;
SelectSample = audio.Samples.Get(@"UI/tabselect-select");
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -71,6 +77,8 @@ namespace osu.Game.Overlays.BeatmapListing
protected override void OnDeactivated() => UpdateState(); protected override void OnDeactivated() => UpdateState();
protected override void OnActivatedByUser() => SelectSample.Play();
/// <summary> /// <summary>
/// Returns the label text to be used for the supplied <paramref name="value"/>. /// Returns the label text to be used for the supplied <paramref name="value"/>.
/// </summary> /// </summary>

View File

@ -198,6 +198,7 @@ namespace osu.Game.Overlays
{ {
c.Anchor = Anchor.TopCentre; c.Anchor = Anchor.TopCentre;
c.Origin = Anchor.TopCentre; c.Origin = Anchor.TopCentre;
c.Scale = new Vector2(0.8f);
})).ToArray(); })).ToArray();
private static ReverseChildIDFillFlowContainer<BeatmapCard> createCardContainerFor(IEnumerable<BeatmapCard> newCards) private static ReverseChildIDFillFlowContainer<BeatmapCard> createCardContainerFor(IEnumerable<BeatmapCard> newCards)

View File

@ -21,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapSet
// propagate value to tab items first to enable only available rulesets. // propagate value to tab items first to enable only available rulesets.
beatmapSet.Value = value; beatmapSet.Value = value;
SelectTab(TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value)); Current.Value = TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value)?.Value;
} }
} }

View File

@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Height = 30; Height = 25;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Children = new Drawable[] Children = new Drawable[]
@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Text = Channel.Name, Text = Channel.Name,
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3, Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 }, Margin = new MarginPadding { Bottom = 2 },
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Chat
public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow; public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow;
protected virtual float FontSize => 20; protected virtual float FontSize => 14;
protected virtual float Spacing => 15; protected virtual float Spacing => 15;

View File

@ -19,6 +19,8 @@ namespace osu.Game.Overlays.Chat
{ {
public partial class ChatTextBar : Container public partial class ChatTextBar : Container
{ {
public const float HEIGHT = 40;
public readonly BindableBool ShowSearch = new BindableBool(); public readonly BindableBool ShowSearch = new BindableBool();
public event Action<string>? OnChatMessageCommitted; public event Action<string>? OnChatMessageCommitted;
@ -45,7 +47,7 @@ namespace osu.Game.Overlays.Chat
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 60; Height = HEIGHT;
Children = new Drawable[] Children = new Drawable[]
{ {
@ -76,7 +78,7 @@ namespace osu.Game.Overlays.Chat
Child = chattingText = new TruncatingSpriteText Child = chattingText = new TruncatingSpriteText
{ {
MaxWidth = chatting_text_width - padding * 2, MaxWidth = chatting_text_width - padding * 2,
Font = OsuFont.Torus.With(size: 20), Font = OsuFont.Torus,
Colour = colourProvider.Background1, Colour = colourProvider.Background1,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
@ -91,7 +93,7 @@ namespace osu.Game.Overlays.Chat
Icon = FontAwesome.Solid.Search, Icon = FontAwesome.Solid.Search,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Size = new Vector2(20), Size = new Vector2(OsuFont.DEFAULT_FONT_SIZE),
Margin = new MarginPadding { Right = 2 }, Margin = new MarginPadding { Right = 2 },
}, },
}, },
@ -101,6 +103,7 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Right = padding }, Padding = new MarginPadding { Right = padding },
Child = chatTextBox = new ChatTextBox Child = chatTextBox = new ChatTextBox
{ {
FontSize = OsuFont.DEFAULT_FONT_SIZE,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Chat.Listing
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;
private const float text_size = 18; private const float text_size = 14;
private const float icon_size = 14; private const float icon_size = 14;
private const float vertical_margin = 1.5f; private const float vertical_margin = 1.5f;

View File

@ -55,7 +55,6 @@ namespace osu.Game.Overlays
private const int transition_length = 500; private const int transition_length = 500;
private const float top_bar_height = 40; private const float top_bar_height = 40;
private const float side_bar_width = 190; private const float side_bar_width = 190;
private const float chat_bar_height = 60;
protected override string PopInSampleName => @"UI/overlay-big-pop-in"; protected override string PopInSampleName => @"UI/overlay-big-pop-in";
protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; protected override string PopOutSampleName => @"UI/overlay-big-pop-out";
@ -136,7 +135,7 @@ namespace osu.Game.Overlays
Padding = new MarginPadding Padding = new MarginPadding
{ {
Left = side_bar_width, Left = side_bar_width,
Bottom = chat_bar_height, Bottom = ChatTextBar.HEIGHT,
}, },
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online; using osu.Game.Online;
using osuTK; using osuTK;
using osuTK.Graphics;
using Realms; using Realms;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
@ -25,6 +26,8 @@ namespace osu.Game.Overlays.FirstRunSetup
private ProgressRoundedButton downloadBundledButton = null!; private ProgressRoundedButton downloadBundledButton = null!;
private ProgressRoundedButton downloadTutorialButton = null!; private ProgressRoundedButton downloadTutorialButton = null!;
private OsuTextFlowContainer downloadInBackgroundText = null!;
private OsuTextFlowContainer currentlyLoadedBeatmaps = null!; private OsuTextFlowContainer currentlyLoadedBeatmaps = null!;
private BundledBeatmapDownloader? tutorialDownloader; private BundledBeatmapDownloader? tutorialDownloader;
@ -100,6 +103,15 @@ namespace osu.Game.Overlays.FirstRunSetup
Text = FirstRunSetupBeatmapScreenStrings.BundledButton, Text = FirstRunSetupBeatmapScreenStrings.BundledButton,
Action = downloadBundled Action = downloadBundled
}, },
downloadInBackgroundText = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Colour = OverlayColourProvider.Light2,
Alpha = 0,
TextAnchor = Anchor.TopCentre,
Text = FirstRunSetupBeatmapScreenStrings.DownloadingInBackground,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
Colour = OverlayColourProvider.Content1, Colour = OverlayColourProvider.Content1,
@ -169,6 +181,10 @@ namespace osu.Game.Overlays.FirstRunSetup
if (bundledDownloader != null) if (bundledDownloader != null)
return; return;
downloadInBackgroundText
.FlashColour(Color4.White, 500)
.FadeIn(200);
bundledDownloader = new BundledBeatmapDownloader(false); bundledDownloader = new BundledBeatmapDownloader(false);
AddInternal(bundledDownloader); AddInternal(bundledDownloader);

View File

@ -128,6 +128,7 @@ namespace osu.Game.Overlays.FirstRunSetup
if (available) if (available)
{ {
copyInformation.Text = FirstRunOverlayImportFromStableScreenStrings.DataMigrationNoExtraSpace; copyInformation.Text = FirstRunOverlayImportFromStableScreenStrings.DataMigrationNoExtraSpace;
copyInformation.AddText(@" "); // just to ensure correct spacing
copyInformation.AddLink(FirstRunOverlayImportFromStableScreenStrings.LearnAboutHardLinks, LinkAction.OpenWiki, @"Client/Release_stream/Lazer/File_storage#via-hard-links"); copyInformation.AddLink(FirstRunOverlayImportFromStableScreenStrings.LearnAboutHardLinks, LinkAction.OpenWiki, @"Client/Release_stream/Lazer/File_storage#via-hard-links");
} }
else if (!RuntimeInfo.IsDesktop) else if (!RuntimeInfo.IsDesktop)

View File

@ -69,6 +69,7 @@ namespace osu.Game.Overlays.Mods
private Task? latestLoadTask; private Task? latestLoadTask;
private ModPanel[]? latestLoadedPanels; private ModPanel[]? latestLoadedPanels;
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && allPanelsLoaded; internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && allPanelsLoaded;
private bool? wasPresent;
private bool allPanelsLoaded private bool allPanelsLoaded
{ {
@ -192,6 +193,15 @@ namespace osu.Game.Overlays.Mods
{ {
base.Update(); base.Update();
// we override `IsPresent` to include the scheduler's pending task state to make async loads work correctly when columns are masked away
// (see description of https://github.com/ppy/osu/pull/19783).
// however, because of that we must also ensure that we signal correct invalidations (https://github.com/ppy/osu-framework/issues/5129).
// failing to do so causes columns to be stuck in "present" mode despite actually not being present themselves.
// this works because `Update()` will always run after a scheduler update, which is what causes the presence state change responsible for the failure.
if (wasPresent != null && wasPresent != IsPresent)
Invalidate(Invalidation.Presence);
wasPresent = IsPresent;
if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay) if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay)
{ {
if (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) if (pendingSelectionOperations.TryDequeue(out var dequeuedAction))

View File

@ -55,7 +55,12 @@ namespace osu.Game.Overlays.Mods
protected override void Select() protected override void Select()
{ {
var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System); // this implicitly presumes that if a system mod declares incompatibility with a non-system mod,
// the non-system mod should take precedence.
// if this assumption is ever broken, this should be reconsidered.
var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System &&
!mod.IncompatibleMods.Any(t => Preset.Value.Mods.Any(t.IsInstanceOfType)));
// will also have the side effect of activating the preset (see `updateActiveState()`). // will also have the side effect of activating the preset (see `updateActiveState()`).
selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray(); selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray();
} }

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