1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 03:42:57 +08:00

Merge branch 'master' into LeaderBoardScore_clean

This commit is contained in:
Dean Herbert 2024-05-27 17:18:41 +09:00 committed by GitHub
commit 740a3377fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
295 changed files with 6095 additions and 2523 deletions

View File

@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2023.1117.0",
"version": "2024.517.0",
"commands": [
"localisation"
]

View File

@ -2,7 +2,6 @@
<Project>
<PropertyGroup Label="C#">
<LangVersion>12.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 438 KiB

View File

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

View File

@ -1,76 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
namespace osu.Android
{
public partial class AndroidJoystickSettings : SettingsSubsection
{
protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad;
private readonly AndroidJoystickHandler joystickHandler;
private readonly Bindable<bool> enabled = new BindableBool(true);
private SettingsSlider<float> deadzoneSlider = null!;
private Bindable<float> handlerDeadzone = null!;
private Bindable<float> localDeadzone = null!;
public AndroidJoystickSettings(AndroidJoystickHandler joystickHandler)
{
this.joystickHandler = joystickHandler;
}
[BackgroundDependencyLoader]
private void load()
{
// use local bindable to avoid changing enabled state of game host's bindable.
handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy();
localDeadzone = handlerDeadzone.GetUnboundCopy();
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = CommonStrings.Enabled,
Current = enabled
},
deadzoneSlider = new SettingsSlider<float>
{
LabelText = JoystickSettingsStrings.DeadzoneThreshold,
KeyboardStep = 0.01f,
DisplayAsPercentage = true,
Current = localDeadzone,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
enabled.BindTo(joystickHandler.Enabled);
enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true);
handlerDeadzone.BindValueChanged(val =>
{
bool disabled = localDeadzone.Disabled;
localDeadzone.Disabled = false;
localDeadzone.Value = val.NewValue;
localDeadzone.Disabled = disabled;
}, true);
localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue);
}
}
}

View File

@ -1,97 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Android.OS;
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
namespace osu.Android
{
public partial class AndroidMouseSettings : SettingsSubsection
{
private readonly AndroidMouseHandler mouseHandler;
protected override LocalisableString Header => MouseSettingsStrings.Mouse;
private Bindable<double> handlerSensitivity = null!;
private Bindable<double> localSensitivity = null!;
private Bindable<bool> relativeMode = null!;
public AndroidMouseSettings(AndroidMouseHandler mouseHandler)
{
this.mouseHandler = mouseHandler;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager osuConfig)
{
// use local bindable to avoid changing enabled state of game host's bindable.
handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy();
localSensitivity = handlerSensitivity.GetUnboundCopy();
relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
// High precision/pointer capture is only available on Android 8.0 and up
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
AddRange(new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.HighPrecisionMouse,
TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
Current = relativeMode,
Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" },
},
new MouseSettings.SensitivitySetting
{
LabelText = MouseSettingsStrings.CursorSensitivity,
Current = localSensitivity,
},
});
}
AddRange(new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel),
},
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
},
});
}
protected override void LoadComplete()
{
base.LoadComplete();
relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true);
handlerSensitivity.BindValueChanged(val =>
{
bool disabled = localSensitivity.Disabled;
localSensitivity.Disabled = false;
localSensitivity.Value = val.NewValue;
localSensitivity.Disabled = disabled;
}, true);
localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
}
}
}

View File

@ -5,13 +5,9 @@ using System;
using Android.App;
using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Input.Handlers;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Updater;
using osu.Game.Utils;
@ -88,24 +84,6 @@ namespace osu.Android
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
switch (handler)
{
case AndroidMouseHandler mh:
return new AndroidMouseSettings(mh);
case AndroidJoystickHandler jh:
return new AndroidJoystickSettings(jh);
case AndroidTouchHandler th:
return new TouchSettings(th);
default:
return base.CreateSettingsSubsectionFor(handler);
}
}
private class AndroidBatteryInfo : BatteryInfo
{
public override double? ChargeLevel => Battery.ChargeLevel;

View File

@ -1,24 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M73.92,44.43C74.82,44.43 75.43,45.1 75.43,46.02V54.54C75.43,55.46 74.82,56.13 73.92,56.13C73,56.13 72.41,55.46 72.41,54.54V46.02C72.41,45.1 73,44.43 73.92,44.43ZM73.92,61.55C72.82,61.55 71.95,60.68 71.95,59.58C71.95,58.51 72.82,57.64 73.92,57.64C75.02,57.64 75.89,58.51 75.89,59.58C75.89,60.68 75.02,61.55 73.92,61.55Z"
android:fillColor="#000000"/>
<path
android:pathData="M68.41,48.55C69.33,48.55 69.92,49.22 69.92,50.11V55.77C69.92,59.94 67.35,61.55 64.22,61.55C61.08,61.55 58.5,59.94 58.5,55.77V50.11C58.5,49.22 59.09,48.55 60.01,48.55C60.91,48.55 61.52,49.22 61.52,50.11V55.56C61.52,57.84 62.48,58.74 64.22,58.74C65.94,58.74 66.9,57.84 66.9,55.56V50.11C66.9,49.22 67.51,48.55 68.41,48.55Z"
android:fillColor="#000000"/>
<path
android:pathData="M49.94,52.01C49.94,52.85 50.81,53.16 52.47,53.54C54.78,54.1 56.98,54.69 56.98,57.53C56.98,60.3 54.93,61.55 51.99,61.55C49.56,61.55 47.79,60.71 46.97,59.73C46.33,58.97 46.41,58.3 47.02,57.71C47.79,56.97 48.46,57.28 48.89,57.66C49.58,58.3 50.43,58.97 52.07,58.97C53.29,58.97 54.06,58.56 54.06,57.74C54.06,56.92 53.24,56.64 51.09,56.05C48.97,55.46 47.08,54.9 47.08,52.36C47.08,49.52 49.38,48.35 51.94,48.35C53.4,48.35 55.06,48.73 56.08,49.83C56.52,50.27 56.85,50.88 56.08,51.72C55.32,52.52 54.75,52.29 54.22,51.88C53.73,51.52 52.88,50.91 51.6,50.91C50.73,50.91 49.94,51.19 49.94,52.01Z"
android:fillColor="#000000"/>
<path
android:pathData="M38.79,61.55C34.9,61.55 32.11,58.74 32.11,54.95C32.11,51.14 34.9,48.35 38.79,48.35C42.68,48.35 45.47,51.14 45.47,54.95C45.47,58.74 42.68,61.55 38.79,61.55ZM38.79,58.74C41.04,58.74 42.45,57.1 42.45,54.95C42.45,52.8 41.04,51.14 38.79,51.14C36.54,51.14 35.13,52.8 35.13,54.95C35.13,57.1 36.54,58.74 38.79,58.74Z"
android:fillColor="#000000"/>
<path
android:pathData="M86,54C86,71.67 71.67,86 54,86C36.33,86 22,71.67 22,54C22,36.33 36.33,22 54,22C71.67,22 86,36.33 86,54ZM25.2,54C25.2,69.91 38.09,82.8 54,82.8C69.91,82.8 82.8,69.91 82.8,54C82.8,38.09 69.91,25.2 54,25.2C38.09,25.2 25.2,38.09 25.2,54Z"
android:fillColor="#000000"/>
<path
android:pathData="M36.78,54.99C36.78,56.09 37.65,56.96 38.75,56.96C39.85,56.96 40.72,56.09 40.72,54.99C40.72,53.91 39.85,53.04 38.75,53.04C37.65,53.04 36.78,53.91 36.78,54.99Z"
android:fillColor="#000000"/>
<vector android:height="108dp" android:viewportHeight="434"
android:viewportWidth="434" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M299.36,178.05C303.08,178.05 305.62,180.81 305.62,184.62V219.92C305.62,223.74 303.08,226.5 299.36,226.5C295.55,226.5 293.11,223.74 293.11,219.92V184.62C293.11,180.81 295.55,178.05 299.36,178.05ZM299.36,248.97C294.81,248.97 291.2,245.37 291.2,240.81C291.2,236.35 294.81,232.75 299.36,232.75C303.92,232.75 307.53,236.35 307.53,240.81C307.53,245.37 303.92,248.97 299.36,248.97Z"/>
<path android:fillColor="#000000" android:pathData="M276.52,195.12C280.34,195.12 282.77,197.87 282.77,201.58V225.01C282.77,242.29 272.12,248.97 259.19,248.97C246.15,248.97 235.49,242.29 235.49,225.01V201.58C235.49,197.87 237.93,195.12 241.75,195.12C245.46,195.12 248,197.87 248,201.58V224.16C248,233.6 251.98,237.31 259.19,237.31C266.29,237.31 270.27,233.6 270.27,224.16V201.58C270.27,197.87 272.81,195.12 276.52,195.12Z"/>
<path android:fillColor="#000000" android:pathData="M200.02,209.43C200.02,212.93 203.63,214.2 210.52,215.79C220.06,218.12 229.18,220.56 229.18,232.33C229.18,243.78 220.7,248.97 208.51,248.97C198.43,248.97 191.12,245.47 187.73,241.44C185.08,238.26 185.4,235.51 187.94,233.07C191.12,229.99 193.88,231.27 195.68,232.86C198.54,235.51 202.04,238.26 208.82,238.26C213.91,238.26 217.09,236.57 217.09,233.18C217.09,229.78 213.7,228.62 204.8,226.18C196,223.74 188.15,221.41 188.15,210.91C188.15,199.15 197.69,194.27 208.29,194.27C214.34,194.27 221.23,195.86 225.47,200.42C227.27,202.22 228.65,204.76 225.47,208.26C222.29,211.55 219.96,210.6 217.73,208.9C215.71,207.41 212.22,204.87 206.92,204.87C203.31,204.87 200.02,206.04 200.02,209.43Z"/>
<path android:fillColor="#000000" android:pathData="M153.74,248.97C138.46,248.97 127.5,237.27 127.5,221.53C127.5,205.68 138.46,194.09 153.74,194.09C169.03,194.09 179.99,205.68 179.99,221.53C179.99,237.27 169.03,248.97 153.74,248.97ZM153.74,237.27C162.59,237.27 168.12,230.46 168.12,221.53C168.12,212.6 162.59,205.68 153.74,205.68C144.89,205.68 139.36,212.6 139.36,221.53C139.36,230.46 144.89,237.27 153.74,237.27Z"/>
<path android:fillColor="#000000" android:pathData="M349,217.5C349,290.13 290.13,349 217.5,349C144.88,349 86,290.13 86,217.5C86,144.88 144.88,86 217.5,86C290.13,86 349,144.88 349,217.5ZM99.15,217.5C99.15,282.86 152.14,335.85 217.5,335.85C282.86,335.85 335.85,282.86 335.85,217.5C335.85,152.14 282.86,99.15 217.5,99.15C152.14,99.15 99.15,152.14 99.15,217.5Z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -164,8 +164,8 @@ namespace osu.Desktop
// user activity
if (activity.Value != null)
{
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
{
@ -271,8 +271,19 @@ namespace osu.Desktop
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
private static string truncate(string str)
private static string clampLength(string str)
{
// Empty strings are fine to discord even though single-character strings are not. Make it make sense.
if (string.IsNullOrEmpty(str))
return str;
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
if (str.Length < 2)
return str.PadRight(2, '\u200B');
if (Encoding.UTF8.GetByteCount(str) <= 128)
return str;

View File

@ -489,6 +489,7 @@ namespace osu.Desktop
public static uint Stride => (uint)Marshal.SizeOf(typeof(NvApplication)) | (2 << 16);
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvStatus
{
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.
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvSystemType
{
UNKNOWN = 0,
@ -618,6 +620,7 @@ namespace osu.Desktop
DESKTOP = 2
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvGpuType
{
UNKNOWN = 0,
@ -625,6 +628,7 @@ namespace osu.Desktop
DGPU = 2, // Discrete
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvSettingID : uint
{
OGL_AA_LINE_GAMMA_ID = 0x2089BF6C,
@ -717,6 +721,7 @@ namespace osu.Desktop
INVALID_SETTING_ID = 0xFFFFFFFF
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvShimSetting : uint
{
SHIM_RENDERING_MODE_INTEGRATED = 0x00000000,
@ -731,6 +736,7 @@ namespace osu.Desktop
SHIM_RENDERING_MODE_DEFAULT = SHIM_RENDERING_MODE_AUTO_SELECT
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum NvThreadControlSetting : uint
{
OGL_THREAD_CONTROL_ENABLE = 0x00000001,

View File

@ -22,7 +22,6 @@ using osu.Game.IPC;
using osu.Game.Online.Multiplayer;
using osu.Game.Performance;
using osu.Game.Utils;
using SDL;
namespace osu.Desktop
{
@ -161,7 +160,7 @@ namespace osu.Desktop
host.Window.Title = Name;
}
protected override BatteryInfo CreateBatteryInfo() => new SDL3BatteryInfo();
protected override BatteryInfo CreateBatteryInfo() => FrameworkEnvironment.UseSDL3 ? new SDL3BatteryInfo() : new SDL2BatteryInfo();
protected override void Dispose(bool isDisposing)
{
@ -169,24 +168,5 @@ namespace osu.Desktop
osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose();
}
private unsafe class SDL3BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
int percentage;
SDL3.SDL_GetPowerInfo(null, &percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
}
}

View File

@ -107,7 +107,13 @@ namespace osu.Desktop
}
}
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null }))
var hostOptions = new HostOptions
{
IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null,
FriendlyGameName = OsuGameBase.GAME_NAME,
};
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, hostOptions))
{
if (!host.IsPrimaryInstance)
{

View File

@ -0,0 +1,25 @@
// 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.Utils;
namespace osu.Desktop
{
internal class SDL2BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
SDL2.SDL.SDL_GetPowerInfo(out _, out int percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL2.SDL.SDL_GetPowerInfo(out _, out _) == SDL2.SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Utils;
using SDL;
namespace osu.Desktop
{
internal unsafe class SDL3BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
int percentage;
SDL3.SDL_GetPowerInfo(null, &percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
@ -163,6 +164,7 @@ namespace osu.Desktop.Windows
[DllImport("Shell32.dll")]
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
[SuppressMessage("ReSharper", "InconsistentNaming")]
private enum EventId
{
/// <summary>
@ -172,6 +174,7 @@ namespace osu.Desktop.Windows
SHCNE_ASSOCCHANGED = 0x08000000
}
[SuppressMessage("ReSharper", "InconsistentNaming")]
private enum Flags : uint
{
SHCNF_IDLIST = 0x0000

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestTouchAreaNotInitiallyVisible()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
}
[Test]
public void TestPressReceptors()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
for (int i = 0; i < 4; i++)
{
int index = i;
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getReceptor(index).Action.Value));
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
}
}
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
public new ManiaPlayfield Playfield => drawableRuleset.Playfield;
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;

View File

@ -93,8 +93,7 @@ namespace osu.Game.Rulesets.Mania.UI
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
// (see `Stage.columnBackgrounds`).
BackgroundContainer,
TopLevelContainer,
new ColumnTouchInputArea(this)
TopLevelContainer
};
var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
@ -181,38 +180,5 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
public partial class ColumnTouchInputArea : Drawable
{
private readonly Column column;
[Resolved(canBeNull: true)]
private ManiaInputManager maniaInputManager { get; set; }
private KeyBindingContainer<ManiaAction> keyBindingContainer;
public ColumnTouchInputArea(Column column)
{
RelativeSizeAxes = Axes.Both;
this.column = column;
}
protected override void LoadComplete()
{
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
keyBindingContainer?.TriggerReleased(column.Action.Value);
}
}
}
}

View File

@ -3,15 +3,12 @@
#nullable disable
using System;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -62,12 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
onSkinChanged();
}
protected override void LoadComplete()
{
base.LoadComplete();
updateMobileSizing();
}
private void onSkinChanged()
{
for (int i = 0; i < stageDefinition.Columns; i++)
@ -92,8 +83,6 @@ namespace osu.Game.Rulesets.Mania.UI
columns[i].Width = width.Value;
}
updateMobileSizing();
}
/// <summary>
@ -106,31 +95,6 @@ namespace osu.Game.Rulesets.Mania.UI
Content[column] = columns[column].Child = content;
}
private void updateMobileSizing()
{
if (!IsLoaded || !RuntimeInfo.IsMobile)
return;
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
// Will be null in tests.
if (containingCell == null)
return;
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
// 1.92 is a "reference" mobile screen aspect ratio for phones.
// We should scale it back for cases like tablets which aren't so extreme.
mobileAdjust *= aspectRatio / 1.92f;
// Best effort until we have better mobile support.
for (int i = 0; i < stageDefinition.Columns; i++)
columns[i].Width *= mobileAdjust;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -31,6 +31,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI
{
[Cached]
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
{
/// <summary>
@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// </summary>
public const double MAX_TIME_RANGE = 11485;
protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
public new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
@ -103,6 +104,8 @@ namespace osu.Game.Rulesets.Mania.UI
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
KeyBindingInputManager.Add(new ManiaTouchInputArea());
}
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;

View File

@ -0,0 +1,216 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
/// <summary>
/// An overlay that captures and displays osu!mania mouse and touch input.
/// </summary>
public partial class ManiaTouchInputArea : VisibilityContainer
{
// visibility state affects our child. we always want to handle input.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
[SettingSource("Spacing", "The spacing between receptors.")]
public BindableFloat Spacing { get; } = new BindableFloat(10)
{
Precision = 1,
MinValue = 0,
MaxValue = 100,
};
[SettingSource("Opacity", "The receptor opacity.")]
public BindableFloat Opacity { get; } = new BindableFloat(1)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 1
};
[Resolved]
private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
private GridContainer gridContainer = null!;
public ManiaTouchInputArea()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
RelativeSizeAxes = Axes.Both;
Height = 0.5f;
}
[BackgroundDependencyLoader]
private void load()
{
List<Drawable> receptorGridContent = new List<Drawable>();
List<Dimension> receptorGridDimensions = new List<Dimension>();
bool first = true;
foreach (var stage in drawableRuleset.Playfield.Stages)
{
foreach (var column in stage.Columns)
{
if (!first)
{
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
}
receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
receptorGridDimensions.Add(new Dimension());
first = false;
}
}
InternalChild = gridContainer = new GridContainer
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Content = new[] { receptorGridContent.ToArray() },
ColumnDimensions = receptorGridDimensions.ToArray()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
Show();
return true;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
Show();
return true;
}
protected override void PopIn()
{
gridContainer.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
gridContainer.FadeOut(300);
}
public partial class ColumnInputReceptor : CompositeDrawable
{
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
private readonly Box highlightOverlay;
[Resolved]
private ManiaInputManager? inputManager { get; set; }
private bool isPressed;
public ColumnInputReceptor()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.15f,
},
highlightOverlay = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
}
}
};
}
protected override bool OnTouchDown(TouchDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnTouchUp(TouchUpEvent e)
{
updateButton(false);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnMouseUp(MouseUpEvent e)
{
updateButton(false);
}
private void updateButton(bool press)
{
if (press == isPressed)
return;
isPressed = press;
if (press)
{
inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
}
else
{
inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value);
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
}
}
}
private partial class Gutter : Drawable
{
public readonly IBindable<float> Spacing = new Bindable<float>();
public Gutter()
{
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
}
}
}
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
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());
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
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]

View File

@ -30,23 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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]
public void TestPerfectCurveTooManyPoints()
{
@ -194,24 +177,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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)
{
AddAssert($"Point at {points.ElementAt(index)} changed",

View File

@ -19,6 +19,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual;
@ -578,6 +579,24 @@ namespace osu.Game.Rulesets.Osu.Tests
assertKeyCounter(1, 1);
}
[Test]
public void TestTouchJudgedCircle()
{
addHitCircleAt(TouchSource.Touch1);
addHitCircleAt(TouchSource.Touch2);
beginTouch(TouchSource.Touch1);
endTouch(TouchSource.Touch1);
// Hold the second touch (this becomes the primary touch).
beginTouch(TouchSource.Touch2);
// Touch again on the first circle.
// Because it's been judged, the cursor should not move here.
beginTouch(TouchSource.Touch1);
checkPosition(TouchSource.Touch2);
}
private void addHitCircleAt(TouchSource source)
{
AddStep($"Add circle at {source}", () =>
@ -590,6 +609,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
Clock = new FramedClock(new ManualClock()),
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
CheckHittable = (_, _, _) => ClickAction.Hit
});
});
}

View File

@ -36,11 +36,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
rhythmStart++;
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
for (int i = rhythmStart; i > 0; i--)
{
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(i + 1);
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
@ -66,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
}
else
{
if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window
if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
startRatio = effectiveRatio;
islandSize = 1;
}
lastObj = prevObj;
prevObj = currObj;
}
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)

View File

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

View File

@ -8,9 +8,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly PathControlPoint ControlPoint;
private readonly T hitObject;
private readonly Container marker;
private readonly Circle circle;
private readonly Drawable markerRing;
[Resolved]
@ -60,38 +60,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
marker = new Container
circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(20),
},
markerRing = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(28),
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
Size = new Vector2(20),
},
markerRing = new CircularProgress
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(28),
Alpha = 0,
InnerRadius = 0.1f,
Progress = 1
}
};
}
@ -115,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
// The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnHover(HoverEvent e)
{
@ -209,8 +193,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (IsHovered || IsSelected.Value)
colour = colour.Lighten(1);
marker.Colour = colour;
marker.Scale = new Vector2(hitObject.Scale);
Colour = colour;
Scale = new Vector2(hitObject.Scale);
}
private Color4 getColourFromNodeType()

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.
internal readonly Container<PathControlPointPiece<T>> Pieces;
internal readonly Container<PathControlPointConnectionPiece<T>> Connections;
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly T hitObject;
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChildren = new Drawable[]
{
Connections = new Container<PathControlPointConnectionPiece<T>> { RelativeSizeAxes = Axes.Both },
new PathControlPointConnection<T>(hitObject),
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);
}
// Generally all the control points are within the visible area all the time.
public override bool UpdateSubTreeMasking() => true;
/// <summary>
/// Handles correction of invalid path types.
/// </summary>
@ -185,17 +187,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
case NotifyCollectionChangedAction.Add:
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++)
{
var point = (PathControlPoint)e.NewItems[i];
@ -209,8 +200,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded;
}));
Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
}
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())
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;
@ -459,10 +435,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
{
changeHandler?.BeginChange();
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type);
EnsureValidPathTypes();
changeHandler?.EndChange();
});
if (countOfState == totalCount)

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
@ -27,14 +28,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public SliderBodyPiece()
{
InternalChild = body = new ManualSliderBody
{
AccentColour = Color4.Transparent
};
AutoSizeAxes = Axes.Both;
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
AlwaysPresent = true;
InternalChild = body = new ManualSliderBody
{
AccentColour = Color4.Transparent
};
}
[BackgroundDependencyLoader]
@ -61,7 +64,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
body.SetVertices(vertices);
}
Size = body.Size;
OriginPosition = body.PathOffset;
}

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
@ -25,33 +24,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSelectionHandler : EditorSelectionHandler
{
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?>? referencePathTypes;
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
SelectionBox.CanFlipX = quad.Width > 0;
SelectionBox.CanFlipY = quad.Height > 0;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
}
protected override void OnOperationEnded()
{
base.OnOperationEnded();
referencePathTypes = null;
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
@ -149,96 +132,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return didFlip;
}
public override bool HandleScale(Vector2 scale, Anchor reference)
{
adjustScaleFromAnchor(ref scale, reference);
var hitObjects = selectedMovableObjects;
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
scaleSlider(slider, scale);
else
scaleHitObjects(hitObjects, reference, scale);
moveSelectionInBounds();
return true;
}
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
{
// cancel out scale in axes we don't care about (based on which drag handle was used).
if ((reference & Anchor.x1) > 0) scale.X = 0;
if ((reference & Anchor.y1) > 0) scale.Y = 0;
// reverse the scale direction if dragging from top or left.
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
private void scaleSlider(Slider slider, Vector2 scale)
{
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
Vector2 pathRelativeDeltaScale = new Vector2(
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
foreach (var point in slider.Path.ControlPoints)
{
oldControlPoints.Enqueue(point.Position);
point.Position *= pathRelativeDeltaScale;
}
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds && slider.Path.HasValidLength)
return;
foreach (var point in slider.Path.ControlPoints)
point.Position = oldControlPoints.Dequeue();
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider);
}
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
scale = getClampedScale(hitObjects, reference, scale);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
foreach (var h in hitObjects)
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
{
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
return (xInBounds, yInBounds);
}
public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
private void moveSelectionInBounds()
{
@ -262,43 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit
h.Position += delta;
}
/// <summary>
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
/// </summary>
/// <param name="hitObjects">The hitobjects to be scaled</param>
/// <param name="reference">The anchor from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <returns>The clamped scale vector</returns>
private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
//max Size -> playfield bounds
if (scaledQuad.TopLeft.X < 0)
scale.X += scaledQuad.TopLeft.X;
if (scaledQuad.TopLeft.Y < 0)
scale.Y += scaledQuad.TopLeft.Y;
if (scaledQuad.BottomRight.X > DrawWidth)
scale.X -= scaledQuad.BottomRight.X - DrawWidth;
if (scaledQuad.BottomRight.Y > DrawHeight)
scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
//min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
Vector2 scaledSize = selectionQuad.Size + scale;
Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
return scale;
}
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>

View File

@ -0,0 +1,220 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSelectionScaleHandler : SelectionScaleHandler
{
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
[BackgroundDependencyLoader]
private void load(EditorBeatmap editorBeatmap)
{
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedItems.CollectionChanged += (_, __) => updateState();
updateState();
}
private void updateState()
{
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
CanScaleX.Value = quad.Width > 0;
CanScaleY.Value = quad.Height > 0;
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
}
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
private Vector2? defaultOrigin;
public override void Begin()
{
if (objectsInScale != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
changeHandler?.BeginChange();
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position))
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
{
var originalInfo = objectsInScale[slider];
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
}
else
{
scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale);
foreach (var (ho, originalState) in objectsInScale)
{
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
}
}
moveSelectionInBounds();
}
public override void Commit()
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
objectsInScale = null;
OriginalSurroundingQuad = null;
defaultOrigin = null;
}
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
.Where(h => h is not Spinner);
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
{
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
{
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
}
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds && slider.Path.HasValidLength)
return;
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
{
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= OsuPlayfield.BASE_SIZE.X);
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= OsuPlayfield.BASE_SIZE.Y);
return (xInBounds, yInBounds);
}
/// <summary>
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
/// </summary>
/// <param name="selectionQuad">The quad surrounding the hitobjects</param>
/// <param name="origin">The origin from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <returns>The clamped scale vector</returns>
private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale)
{
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin);
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin);
var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin);
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0))
scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0))
scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0))
scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0))
scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
}
private void moveSelectionInBounds()
{
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
Vector2 delta = Vector2.Zero;
if (quad.TopLeft.X < 0)
delta.X -= quad.TopLeft.X;
if (quad.TopLeft.Y < 0)
delta.Y -= quad.TopLeft.Y;
if (quad.BottomRight.X > OsuPlayfield.BASE_SIZE.X)
delta.X -= quad.BottomRight.X - OsuPlayfield.BASE_SIZE.X;
if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y)
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
foreach (var (h, _) in objectsInScale!)
h.Position += delta;
}
private struct OriginalHitObjectState
{
public Vector2 Position { get; }
public Vector2[]? PathControlPointPositions { get; }
public PathType?[]? PathControlPointTypes { get; }
public OriginalHitObjectState(OsuHitObject hitObject)
{
Position = hitObject.Position;
PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray();
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
}
}
}
}

View File

@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu
//
// Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected),
// this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can.
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.ReceivePositionalInputAt(screenSpacePosition));
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.CanBeHit() && c.ReceivePositionalInputAt(screenSpacePosition));
public OsuInputManager(RulesetInfo ruleset)
: base(ruleset, 0, SimultaneousBindingMode.Unique)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Default
@ -11,10 +12,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// </summary>
public partial class ManualSliderBody : SliderBody
{
public new void SetVertices(IReadOnlyList<Vector2> vertices)
public ManualSliderBody()
{
base.SetVertices(vertices);
Size = Path.Size;
AutoSizeAxes = Axes.Both;
}
public new void SetVertices(IReadOnlyList<Vector2> vertices) => base.SetVertices(vertices);
}
}

View File

@ -8,7 +8,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
// For osu! gameplay, everything is always on screen.
// Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245)
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking() => false;
public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; }

View File

@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
}
protected override double ComputeTimeRange()
{
// Adjust when we're using constant algorithm to not be sluggish.
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
return base.ComputeTimeRange() / multiplier;
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModConstantSpeed : Mod, IApplicableToDrawableRuleset<TaikoHitObject>
{
public override string Name => "Constant Speed";
public override string Acronym => "CS";
public override double ScoreMultiplier => 0.9;
public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
public override ModType Type => ModType.Conversion;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
var taikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
taikoRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Alpha = 0,
Scale = new Vector2(0.7f),
Scale = new Vector2(TaikoLegacyHitTarget.SCALE),
Colour = new Colour4(255, 228, 0, 255),
};
@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
if (!result.IsHit || !isKiaiActive)
return;
sprite.ScaleTo(0.85f).Then()
.ScaleTo(0.7f, 80, Easing.OutQuad);
sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then()
.ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad);
}
}
}

View File

@ -12,6 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public partial class TaikoLegacyHitTarget : CompositeDrawable
{
/// <summary>
/// In stable this is 0.7f (see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L592)
/// but for whatever reason this doesn't match visually.
/// </summary>
public const float SCALE = 0.8f;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
@ -22,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.83f),
Scale = new Vector2(SCALE + 0.03f),
Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -30,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.8f),
Scale = new Vector2(SCALE),
Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko
new TaikoModClassic(),
new TaikoModSwap(),
new TaikoModSingleTap(),
new TaikoModConstantSpeed(),
};
case ModType.Automation:

View File

@ -82,7 +82,12 @@ namespace osu.Game.Rulesets.Taiko.UI
TimeRange.Value = ComputeTimeRange();
}
protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange();
protected virtual double ComputeTimeRange()
{
// Adjust when we're using constant algorithm to not be sluggish.
double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant ? 4 * Beatmap.Difficulty.SliderMultiplier : 1;
return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier;
}
protected override void UpdateAfterChildren()
{

View File

@ -7,7 +7,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
@ -345,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public void Add(Drawable proxy) => AddInternal(proxy);
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
public override bool UpdateSubTreeMasking()
{
// DrawableHitObject disables masking.
// Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.

View File

@ -1188,5 +1188,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
}
}
[Test]
public void TestBeatmapDifficultyIsClamped()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("out-of-range-difficulties.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream).Difficulty;
Assert.That(decoded.DrainRate, Is.EqualTo(10));
Assert.That(decoded.CircleSize, Is.EqualTo(10));
Assert.That(decoded.OverallDifficulty, Is.EqualTo(10));
Assert.That(decoded.ApproachRate, Is.EqualTo(10));
Assert.That(decoded.SliderMultiplier, Is.EqualTo(3.6));
Assert.That(decoded.SliderTickRate, Is.EqualTo(8));
}
}
[Test]
public void TestManiaBeatmapDifficultyCircleSizeClamp()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("out-of-range-difficulties-mania.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream).Difficulty;
Assert.That(decoded.CircleSize, Is.EqualTo(14));
}
}
}
}

View File

@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources;
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));
[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))]
public void TestEncodeDecodeStability(string name)
{

View File

@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO.Legacy;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
@ -31,6 +32,7 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources;
using osu.Game.Users;
namespace osu.Game.Tests.Beatmaps.Formats
{
@ -224,6 +226,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.User = new APIUser
{
Username = "spaceman_atlas",
Id = 3035836,
CountryCode = CountryCode.PL
};
scoreInfo.ClientVersion = "2023.1221.0";
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.Mods, Is.EqualTo(scoreInfo.Mods));
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.LargeTickHit] = 1,
};
scoreInfo.Rank = ScoreRank.A;
var beatmap = new TestBeatmap(ruleset);
var score = new Score
@ -412,6 +422,80 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
[Test]
public void TestTotalScoreWithoutModsReadIfPresent()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[]
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.ClientVersion = "2023.1221.0";
scoreInfo.TotalScoreWithoutMods = 1_000_000;
scoreInfo.TotalScore = 1_020_000;
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
});
}
[Test]
public void TestTotalScoreWithoutModsBackwardsPopulatedIfMissing()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[]
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.ClientVersion = "2023.1221.0";
scoreInfo.TotalScoreWithoutMods = 0;
scoreInfo.TotalScore = 1_020_000;
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
});
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestCase(1, 3)]
[TestCase(1, 0)]
[TestCase(0, 3)]
public void CatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
public void TestCatchMergesFruitAndDropletMisses(int missCount, int largeTickMissCount)
{
var ruleset = new CatchRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
@ -41,7 +41,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
[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 scoreInfo = TestResources.CreateTestScoreInfo(ruleset);

View File

@ -0,0 +1,5 @@
[General]
Mode: 3
[Difficulty]
CircleSize:14

View File

@ -0,0 +1,10 @@
[General]
Mode: 0
[Difficulty]
HPDrainRate:25
CircleSize:25
OverallDifficulty:25
ApproachRate:30
SliderMultiplier:30
SliderTickRate:30

View File

@ -15,6 +15,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@ -23,6 +24,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources;
using osu.Game.Users;
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)
{
// 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.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@ -12,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using MemoryStream = System.IO.MemoryStream;
namespace osu.Game.Tests.Skins
{
@ -21,6 +23,52 @@ namespace osu.Game.Tests.Skins
[Resolved]
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]
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);
}
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)
{
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();

View File

@ -10,9 +10,11 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;
@ -26,9 +28,13 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(SelectionRotationHandler))]
private TestSelectionRotationHandler rotationHandler;
[Cached(typeof(SelectionScaleHandler))]
private TestSelectionScaleHandler scaleHandler;
public TestSceneComposeSelectBox()
{
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
scaleHandler = new TestSelectionScaleHandler(() => selectionArea);
}
[SetUp]
@ -45,13 +51,8 @@ namespace osu.Game.Tests.Visual.Editing
{
RelativeSizeAxes = Axes.Both,
CanScaleX = true,
CanScaleY = true,
CanScaleDiagonally = true,
CanFlipX = true,
CanFlipY = true,
OnScale = handleScale
}
}
};
@ -60,27 +61,6 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseButton(MouseButton.Left);
});
private bool handleScale(Vector2 amount, Anchor reference)
{
if ((reference & Anchor.y1) == 0)
{
int directionY = (reference & Anchor.y0) > 0 ? -1 : 1;
if (directionY < 0)
selectionArea.Y += amount.Y;
selectionArea.Height += directionY * amount.Y;
}
if ((reference & Anchor.x1) == 0)
{
int directionX = (reference & Anchor.x0) > 0 ? -1 : 1;
if (directionX < 0)
selectionArea.X += amount.X;
selectionArea.Width += directionX * amount.X;
}
return true;
}
private partial class TestSelectionRotationHandler : SelectionRotationHandler
{
private readonly Func<Container> getTargetContainer;
@ -125,6 +105,51 @@ namespace osu.Game.Tests.Visual.Editing
}
}
private partial class TestSelectionScaleHandler : SelectionScaleHandler
{
private readonly Func<Container> getTargetContainer;
public TestSelectionScaleHandler(Func<Container> getTargetContainer)
{
this.getTargetContainer = getTargetContainer;
CanScaleX.Value = true;
CanScaleY.Value = true;
CanScaleDiagonally.Value = true;
}
[CanBeNull]
private Container targetContainer;
public override void Begin()
{
if (targetContainer != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
targetContainer = getTargetContainer();
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
Vector2 actualOrigin = origin ?? Vector2.Zero;
targetContainer.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft);
targetContainer.Size = OriginalSurroundingQuad!.Value.Size * scale;
}
public override void Commit()
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
targetContainer = null;
}
}
[Test]
public void TestRotationHandleShownOnHover()
{

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Collections.Generic;
using Humanizer;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@ -396,7 +397,7 @@ namespace osu.Game.Tests.Visual.Editing
textBox.Current.Value = bank;
// force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
InputManager.ChangeFocus(textBox);
((IFocusManager)InputManager).ChangeFocus(textBox);
InputManager.Key(Key.Enter);
});

View File

@ -6,6 +6,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.UserInterface;
@ -62,12 +63,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("drop focus", () => InputManager.ChangeFocus(null));
AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
}
@ -77,12 +78,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("drop focus", () => InputManager.ChangeFocus(null));
AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
}

View File

@ -3,17 +3,22 @@
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneMetadataSection : OsuTestScene
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
{
[Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
@ -26,6 +31,81 @@ namespace osu.Game.Tests.Visual.Editing
private TestMetadataSection metadataSection;
[Test]
public void TestUpdateViaTextBoxOnFocusLoss()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.Artist = "Example Artist";
editorBeatmap.Metadata.ArtistUnicode = string.Empty;
});
createSection();
TextBox textbox;
AddStep("focus first textbox", () =>
{
textbox = metadataSection.ChildrenOfType<TextBox>().First();
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
assertArtistMetadata("Example Artist");
// It's important values are committed immediately on focus loss so the editor exit sequence detects them.
AddAssert("value immediately changed on focus loss", () =>
{
((IFocusManager)InputManager).TriggerFocusContention(metadataSection);
return editorBeatmap.Metadata.Artist;
}, () => Is.EqualTo("Example ArtistExample Artist"));
}
[Test]
public void TestUpdateViaTextBoxOnCommit()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.Artist = "Example Artist";
editorBeatmap.Metadata.ArtistUnicode = string.Empty;
});
createSection();
TextBox textbox;
AddStep("focus first textbox", () =>
{
textbox = metadataSection.ChildrenOfType<TextBox>().First();
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
assertArtistMetadata("Example Artist");
AddStep("commit", () => InputManager.Key(Key.Enter));
assertArtistMetadata("Example ArtistExample Artist");
}
[Test]
public void TestMinimalMetadata()
{
@ -40,7 +120,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection();
assertArtist("Example Artist");
assertArtistTextBox("Example Artist");
assertRomanisedArtist("Example Artist", false);
assertTitle("Example Title");
@ -61,7 +141,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection();
assertArtist("*なみりん");
assertArtistTextBox("*なみりん");
assertRomanisedArtist(string.Empty, true);
assertTitle("コイシテイク・プラネット");
@ -82,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection();
assertArtist("*なみりん");
assertArtistTextBox("*なみりん");
assertRomanisedArtist("*namirin", true);
assertTitle("コイシテイク・プラネット");
@ -104,11 +184,11 @@ namespace osu.Game.Tests.Visual.Editing
createSection();
AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin");
assertArtist("*namirin");
assertArtistTextBox("*namirin");
assertRomanisedArtist("*namirin", false);
AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん");
assertArtist("*なみりん");
assertArtistTextBox("*なみりん");
assertRomanisedArtist("*namirin", true);
AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori");
@ -123,21 +203,24 @@ namespace osu.Game.Tests.Visual.Editing
private void createSection()
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
private void assertArtist(string expected)
=> AddAssert($"artist is {expected}", () => metadataSection.ArtistTextBox.Current.Value == expected);
private void assertArtistMetadata(string expected)
=> AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
private void assertArtistTextBox(string expected)
=> AddAssert($"artist textbox is {expected}", () => metadataSection.ArtistTextBox.Current.Value, () => Is.EqualTo(expected));
private void assertRomanisedArtist(string expected, bool editable)
{
AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value == expected);
AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value, () => Is.EqualTo(expected));
AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable);
}
private void assertTitle(string expected)
=> AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value == expected);
=> AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value, () => Is.EqualTo(expected));
private void assertRomanisedTitle(string expected, bool editable)
{
AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value == expected);
AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value, () => Is.EqualTo(expected));
AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable);
}

View File

@ -85,8 +85,8 @@ namespace osu.Game.Tests.Visual.Gameplay
if (scaleTransformProvided)
{
sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current, Time.Current + 1000, 1, 2);
sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1);
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();
@ -211,7 +211,8 @@ namespace osu.Game.Tests.Visual.Gameplay
var layer = storyboard.GetLayer("Background");
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.Add(sprite);

View File

@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay
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);
@ -73,17 +73,17 @@ namespace osu.Game.Tests.Visual.Gameplay
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
// 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);
var loopGroup = sprite.AddLoop(loop_start_time, 50);
loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1);
sprite.Commands.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
var loopGroup = sprite.AddLoopingGroup(loop_start_time, 50);
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;
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.
sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1);
sprite.Commands.AddAlpha(Easing.None, 18000, 20000, 0, 1);
loopGroup.AddAlpha(Easing.None, 38000, 40000, 0, 1);
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 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);
return storyboard;
}

View File

@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var storyboard = new Storyboard();
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);
return storyboard;
}

View File

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

View File

@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestIntroStoryboardElement() => testLeadIn(b =>
{
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);
});

View File

@ -7,6 +7,7 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
@ -27,6 +29,59 @@ namespace osu.Game.Tests.Visual.Navigation
{
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
{
[Test]
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
{
BeatmapSetInfo beatmapSet = null!;
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("wait for song select",
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
&& songSelect.IsLoaded);
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
AddStep("change to song setup", () => InputManager.Key(Key.F4));
TextBox textbox = null!;
AddUntilStep("wait for metadata section", () =>
{
var t = Game.ChildrenOfType<MetadataSection>().SingleOrDefault().ChildrenOfType<TextBox>().FirstOrDefault();
if (t == null)
return false;
textbox = t;
return true;
});
AddStep("focus textbox", () =>
{
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
AddStep("exit", () => Game.ChildrenOfType<Editor>().Single().Exit());
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
}
[Test]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
@ -26,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
public partial class TestScenePresentScore : OsuGameTestScene
{
private BeatmapSetInfo beatmap;
private BeatmapSetInfo beatmap = null!;
[SetUpSteps]
public new void SetUpSteps()
@ -64,7 +62,7 @@ namespace osu.Game.Tests.Visual.Navigation
Ruleset = new OsuRuleset().RulesetInfo
},
}
})?.Value;
})!.Value;
});
}
@ -145,6 +143,40 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(secondImport, type);
}
[Test]
public void TestPresentTwoImportsWithSameOnlineIDButDifferentHashes([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
var firstImport = importScore(1);
presentAndConfirm(firstImport, type);
var secondImport = importScore(1);
presentAndConfirm(secondImport, type);
}
[Test]
public void TestScoreRefetchIgnoresEmptyHash()
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
importScore(-1, hash: string.Empty);
importScore(3, hash: @"deadbeef");
// oftentimes a `PresentScore()` call will be given a `ScoreInfo` which is converted from an online score,
// in which cases the hash will generally not be available.
AddStep("present score", () => Game.PresentScore(new ScoreInfo { OnlineID = 3, Hash = string.Empty }));
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
AddUntilStep("correct score displayed", () =>
{
var score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score!;
return score.OnlineID == 3 && score.Hash == "deadbeef";
});
}
private void returnToMenu()
{
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
@ -158,14 +190,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
}
private Func<ScoreInfo> importScore(int i, RulesetInfo ruleset = null)
private Func<ScoreInfo> importScore(int i, RulesetInfo? ruleset = null, string? hash = null)
{
ScoreInfo imported = null;
ScoreInfo? imported = null;
AddStep($"import score {i}", () =>
{
imported = Game.ScoreManager.Import(new ScoreInfo
{
Hash = Guid.NewGuid().ToString(),
Hash = hash ?? Guid.NewGuid().ToString(),
OnlineID = i,
BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo,
@ -175,14 +207,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert($"import {i} succeeded", () => imported != null);
return () => imported;
return () => imported!;
}
/// <summary>
/// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time.
/// There's a case where they may succeed incorrectly if we don't compare against the previous instance.
/// </summary>
private IScreen lastWaitedScreen;
private IScreen lastWaitedScreen = null!;
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
{

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
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));
}
[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()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());

View File

@ -87,6 +87,105 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("delete all beatmaps", () => manager.Delete());
}
[Test]
public void TestSpeedChange()
{
createSongSelect();
changeMods();
decreaseModSpeed();
AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
decreaseModSpeed();
AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
increaseModSpeed();
AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
increaseModSpeed();
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
increaseModSpeed();
AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
increaseModSpeed();
AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
decreaseModSpeed();
AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
OsuModNightcore nc = new OsuModNightcore
{
SpeedChange = { Value = 1.05 }
};
changeMods(nc);
increaseModSpeed();
AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
decreaseModSpeed();
AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
decreaseModSpeed();
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
decreaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
decreaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
increaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
OsuModDoubleTime dt = new OsuModDoubleTime
{
SpeedChange = { Value = 1.02 },
AdjustPitch = { Value = true },
};
changeMods(dt);
decreaseModSpeed();
AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005));
AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
OsuModHalfTime ht = new OsuModHalfTime
{
SpeedChange = { Value = 0.97 },
AdjustPitch = { Value = true },
};
Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() };
changeMods(modlist);
increaseModSpeed();
AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005));
AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.True);
AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType<ModHidden>().SingleOrDefault(), () => Is.Not.Null);
AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType<ModHardRock>().SingleOrDefault(), () => Is.Not.Null);
changeMods(new ModWindUp());
increaseModSpeed();
AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp);
changeMods(new ModAdaptiveSpeed());
increaseModSpeed();
AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed);
void increaseModSpeed() => AddStep("increase mod speed", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Up);
InputManager.ReleaseKey(Key.ControlLeft);
});
void decreaseModSpeed() => AddStep("decrease mod speed", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Down);
InputManager.ReleaseKey(Key.ControlLeft);
});
}
[Test]
public void TestPlaceholderBeatmapPresence()
{

View File

@ -0,0 +1,83 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Screens.Menu;
using osuTK.Input;
using Color4 = osuTK.Graphics.Color4;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneMainMenuButton : OsuTestScene
{
[Resolved]
private MetadataClient metadataClient { get; set; } = null!;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Test]
public void TestStandardButton()
{
AddStep("add button", () => Child = new MainMenuButton(
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
}
[Test]
public void TestDailyChallengeButton()
{
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddStep("set up API", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case GetRoomRequest getRoomRequest:
if (getRoomRequest.RoomId != 1234)
return false;
var beatmap = CreateAPIBeatmap();
beatmap.OnlineID = 1001;
getRoomRequest.TriggerSuccess(new Room
{
RoomID = { Value = 1234 },
Playlist =
{
new PlaylistItem(beatmap)
},
EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) }
});
return true;
default:
return false;
}
});
AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234,
}));
}
}
}

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 } } });
}
[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)
=> AddAssert("selected mods changed correctly", () => new HashSet<Mod>(SelectedMods.Value).SetEquals(mods));

View File

@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Utils;
@ -623,7 +624,7 @@ namespace osu.Game.Tests.Visual.UserInterface
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("unfocus search text box externally", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);

View File

@ -5,7 +5,9 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
@ -20,9 +22,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private NowPlayingOverlay nowPlayingOverlay;
[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
{
@ -37,9 +39,38 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestShowHideDisable()
{
AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
AddStep(@"show", () => nowPlayingOverlay.Show());
AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state);
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

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -10,16 +12,19 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Select.FooterV2;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.SelectV2.Footer;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSongSelectFooterV2 : OsuManualInputManagerTestScene
public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene
{
private FooterButtonRandomV2 randomButton = null!;
private FooterButtonModsV2 modsButton = null!;
private ScreenFooterButtonRandom randomButton = null!;
private ScreenFooterButtonMods modsButton = null!;
private bool nextRandomCalled;
private bool previousRandomCalled;
@ -35,25 +40,25 @@ namespace osu.Game.Tests.Visual.SongSelect
nextRandomCalled = false;
previousRandomCalled = false;
FooterV2 footer;
ScreenFooter footer;
Children = new Drawable[]
{
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = footer = new FooterV2(),
Child = footer = new ScreenFooter(),
},
overlay = new DummyOverlay()
};
footer.AddButton(modsButton = new FooterButtonModsV2(), overlay);
footer.AddButton(randomButton = new FooterButtonRandomV2
footer.AddButton(modsButton = new ScreenFooterButtonMods { Current = SelectedMods }, overlay);
footer.AddButton(randomButton = new ScreenFooterButtonRandom
{
NextRandom = () => nextRandomCalled = true,
PreviousRandom = () => previousRandomCalled = true
});
footer.AddButton(new FooterButtonOptionsV2());
footer.AddButton(new ScreenFooterButtonOptions());
overlay.Hide();
});
@ -61,15 +66,40 @@ namespace osu.Game.Tests.Visual.SongSelect
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)));
}
[Test]
public void TestMods()
{
AddStep("one mod", () => SelectedMods.Value = new List<Mod> { new OsuModHidden() });
AddStep("two mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock() });
AddStep("three mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
AddStep("four mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() });
AddStep("five mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() });
AddStep("modified", () => SelectedMods.Value = new List<Mod> { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("modified + one", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("modified + two", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("modified + three", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("modified + four", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } });
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddWaitStep("wait", 3);
AddStep("one mod", () => SelectedMods.Value = new List<Mod> { new OsuModHidden() });
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddWaitStep("wait", 3);
AddStep("five mods", () => SelectedMods.Value = new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() });
}
[Test]
public void TestShowOptions()
{
AddStep("enable options", () =>
{
var optionsButton = this.ChildrenOfType<FooterButtonV2>().Last();
var optionsButton = this.ChildrenOfType<ScreenFooterButton>().Last();
optionsButton.Enabled.Value = true;
optionsButton.TriggerClick();
@ -79,7 +109,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestState()
{
AddToggleStep("set options enabled state", state => this.ChildrenOfType<FooterButtonV2>().Last().Enabled.Value = state);
AddToggleStep("set options enabled state", state => this.ChildrenOfType<ScreenFooterButton>().Last().Enabled.Value = state);
}
[Test]

View File

@ -0,0 +1,120 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.SelectV2.Footer;
using osu.Game.Utils;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneScreenFooterButtonMods : OsuTestScene
{
private readonly TestScreenFooterButtonMods footerButtonMods;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
public TestSceneScreenFooterButtonMods()
{
Add(footerButtonMods = new TestScreenFooterButtonMods
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
X = -100,
Action = () => { },
});
}
[Test]
public void TestDisplay()
{
AddStep("one mod", () => changeMods(new List<Mod> { new OsuModHidden() }));
AddStep("two mods", () => changeMods(new List<Mod> { new OsuModHidden(), new OsuModHardRock() }));
AddStep("three mods", () => changeMods(new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }));
AddStep("four mods", () => changeMods(new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic() }));
AddStep("five mods", () => changeMods(new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }));
AddStep("modified", () => changeMods(new List<Mod> { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }));
AddStep("modified + one", () => changeMods(new List<Mod> { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }));
AddStep("modified + two", () => changeMods(new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }));
AddStep("clear mods", () => changeMods(Array.Empty<Mod>()));
AddWaitStep("wait", 3);
AddStep("one mod", () => changeMods(new List<Mod> { new OsuModHidden() }));
AddStep("clear mods", () => changeMods(Array.Empty<Mod>()));
AddWaitStep("wait", 3);
AddStep("five mods", () => changeMods(new List<Mod> { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime(), new OsuModClassic(), new OsuModDifficultyAdjust() }));
}
[Test]
public void TestIncrementMultiplier()
{
var hiddenMod = new Mod[] { new OsuModHidden() };
AddStep(@"Add Hidden", () => changeMods(hiddenMod));
AddAssert(@"Check Hidden multiplier", () => assertModsMultiplier(hiddenMod));
var hardRockMod = new Mod[] { new OsuModHardRock() };
AddStep(@"Add HardRock", () => changeMods(hardRockMod));
AddAssert(@"Check HardRock multiplier", () => assertModsMultiplier(hardRockMod));
var doubleTimeMod = new Mod[] { new OsuModDoubleTime() };
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod));
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods));
}
[Test]
public void TestDecrementMultiplier()
{
var easyMod = new Mod[] { new OsuModEasy() };
AddStep(@"Add Easy", () => changeMods(easyMod));
AddAssert(@"Check Easy multiplier", () => assertModsMultiplier(easyMod));
var noFailMod = new Mod[] { new OsuModNoFail() };
AddStep(@"Add NoFail", () => changeMods(noFailMod));
AddAssert(@"Check NoFail multiplier", () => assertModsMultiplier(noFailMod));
var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() };
AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods));
AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleDecrementMods));
}
[Test]
public void TestUnrankedBadge()
{
AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() }));
AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType<ScreenFooterButtonMods.UnrankedBadge>().Single().Alpha == 1);
AddStep(@"Clear selected mod", () => changeMods(Array.Empty<Mod>()));
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<ScreenFooterButtonMods.UnrankedBadge>().Single().Alpha == 0);
}
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
private bool assertModsMultiplier(IEnumerable<Mod> mods)
{
double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString();
return expectedValue == footerButtonMods.MultiplierText.Current.Value;
}
private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods
{
public new OsuSpriteText MultiplierText => base.MultiplierText;
}
}
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
@ -61,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -71,12 +72,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null));
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -87,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
@ -106,7 +107,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -116,12 +117,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null));
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));

View File

@ -114,6 +114,51 @@ namespace osu.Game.Tests.Visual.UserInterface
=> 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]
public void TestCustomState()
{

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection =>
{
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
GetContainingInputManager().TriggerFocusContention(null);
GetContainingFocusManager().TriggerFocusContention(null);
// Required to avoid cyclic failure in BindableWithCurrent (TriggerChange called during the Current_Set process).
// Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best.

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Screens.Ladder
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking() => false;
protected override void OnDrag(DragEvent e)
{

View File

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

View File

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

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
@ -21,6 +22,8 @@ using osu.Game.Utils;
namespace osu.Game.Beatmaps.Drawables
{
[SuppressMessage("ReSharper", "StringLiteralTypo")]
[SuppressMessage("ReSharper", "CommentTypo")]
public partial class BundledBeatmapDownloader : CompositeDrawable
{
private readonly bool shouldPostNotifications;
@ -50,7 +53,7 @@ namespace osu.Game.Beatmaps.Drawables
{
queueDownloads(always_bundled_beatmaps);
queueDownloads(bundled_osu, 8);
queueDownloads(bundled_osu, 6);
queueDownloads(bundled_taiko, 3);
queueDownloads(bundled_catch, 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";
/// <summary>
@ -135,215 +158,312 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary>
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",
// 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",
// 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 =
{
"682286 Yuyoyuppe - Emerald Galaxy.osz",
"682287 baker - For a Dead Girl+.osz",
"682289 Hige Driver - I Wanna Feel Your Love (feat. shully).osz",
"682290 Hige Driver - Miracle Sugite Yabai (feat. shully).osz",
"682416 Hige Driver - Palette.osz",
"682595 baker - Kimi ga Kimi ga -vocanico remix-.osz",
"716211 yuki. - Spring Signal.osz",
"716213 dark cat - BUBBLE TEA (feat. juu & cinders).osz",
"716215 LukHash - CLONED.osz",
"716219 IAHN - Snowdrop.osz",
"716249 *namirin - Senaka Awase no Kuukyo (with Kakichoco).osz",
"716390 sakuraburst - SHA.osz",
"716441 Fractal Dreamers - Paradigm Shift.osz",
"729808 Thaehan - Leprechaun.osz",
"751771 Cranky - Hanaarashi.osz",
"751772 Cranky - Ran.osz",
"751773 Cranky - Feline, the White....osz",
"751774 Function Phantom - Variable.osz",
"751779 Rin - Daishibyo set 14 ~ Sado no Futatsuiwa.osz",
"751782 Fractal Dreamers - Fata Morgana.osz",
"751785 Cranky - Chandelier - King.osz",
"751846 Fractal Dreamers - Celestial Horizon.osz",
"751866 Rin - Moriya set 08 ReEdit ~ Youkai no Yama.osz",
"751894 Fractal Dreamers - Blue Haven.osz",
"751896 Cranky - Rave 2 Rave.osz",
"751932 Cranky - La fuite des jours.osz",
"751972 Cranky - CHASER.osz",
"779173 Thaehan - Superpower.osz",
"780932 VINXIS - A Centralized View.osz",
"785572 S3RL - I'll See You Again (feat. Chi Chi).osz",
"785650 yuki. feat. setsunan - Hello! World.osz",
"785677 Dictate - Militant.osz",
"785731 S3RL - Catchit (Radio Edit).osz",
"785774 LukHash - GLITCH.osz",
"786498 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz",
"789374 Pulse - LP.osz",
"789528 James Portland - Sky.osz",
"789529 Lexurus - Gravity.osz",
"789544 Andromedik - Invasion.osz",
"789905 Gourski x Himmes - Silence.osz",
"791667 cYsmix - Babaroque (Short Ver.).osz",
"791798 cYsmix - Behind the Walls.osz",
"791845 cYsmix - Little Knight.osz",
"792241 cYsmix - Eden.osz",
"792396 cYsmix - The Ballad of a Mindless Girl.osz",
"795432 Phonetic - Journey.osz",
"831322 DJ'TEKINA//SOMETHING - Hidamari no Uta.osz",
"847764 Cranky - Crocus.osz",
"847776 Culprate & Joe Ford - Gaucho.osz",
"847812 J. Pachelbel - Canon (Cranky Remix).osz",
"847900 Cranky - Time Alter.osz",
"847930 LukHash - 8BIT FAIRY TALE.osz",
"848003 Culprate - Aurora.osz",
"848068 nanobii - popsicle beach.osz",
"848090 Trial & Error - DAI*TAN SENSATION feat. Nanahira, Mii, Aitsuki Nakuru (Short Ver.).osz",
"848259 Culprate & Skorpion - Jester.osz",
"848976 Dictate - Treason.osz",
"851543 Culprate - Florn.osz",
"864748 Thaehan - Angry Birds Epic (Remix).osz",
"873667 OISHII - ONIGIRI FREEWAY.osz",
"876227 Culprate, Keota & Sophie Meiers - Mechanic Heartbeat.osz",
"880487 cYsmix - Peer Gynt.osz",
"883088 Wisp X - Somewhere I'd Rather Be.osz",
"891333 HyuN - White Aura.osz",
"891334 HyuN - Wild Card.osz",
"891337 HyuN feat. LyuU - Cross Over.osz",
"891338 HyuN & Ritoru - Apocalypse in Love.osz",
"891339 HyuN feat. Ato - Asu wa Ame ga Yamukara.osz",
"891345 HyuN - Infinity Heaven.osz",
"891348 HyuN - Guitian.osz",
"891356 HyuN - Legend of Genesis.osz",
"891366 HyuN - Illusion of Inflict.osz",
"891417 HyuN feat. Yu-A - My life is for you.osz",
"891441 HyuN - You'Re aRleAdY dEAd.osz",
"891632 HyuN feat. YURI - Disorder.osz",
"891712 HyuN - Tokyo's Starlight.osz",
"901091 *namirin - Ciel etoile.osz",
"916990 *namirin - Koishiteiku Planet.osz",
"929284 tieff - Sense of Nostalgia.osz",
"933940 Ben Briggs - Yes (Maybe).osz",
"934415 Ben Briggs - Fearless Living.osz",
"934627 Ben Briggs - New Game Plus.osz",
"934666 Ben Briggs - Wave Island.osz",
"936126 siromaru + cranky - conflict.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",
@"682286 Yuyoyuppe - Emerald Galaxy.osz",
@"682287 baker - For a Dead Girl+.osz",
@"682595 baker - Kimi ga Kimi ga -vocanico remix-.osz",
@"1048705 Thaehan - Never Give Up.osz",
@"1050185 Carpool Tunnel - Hooked Again.osz",
@"1052846 Carpool Tunnel - Impressions.osz",
@"1062477 Ricky Montgomery - Line Without a Hook.osz",
@"1081119 Celldweller - Pulsar.osz",
@"1086289 Frums - 24eeev0-$.osz",
@"1133317 PUP - Free At Last.osz",
@"1171188 PUP - Full Blown Meltdown.osz",
@"1177043 PUP - My Life Is Over And I Couldn't Be Happier.osz",
@"1250387 Circle of Dust - Humanarchy (Cut Ver.).osz",
@"1255411 Wisp X - Somewhere I'd Rather Be.osz",
@"1320298 nekodex - Little Drummer Girl.osz",
@"1323877 Masahiro ""Godspeed"" Aoki - Blaze.osz",
@"1342280 Minagu feat. Aitsuki Nakuru - Theater Endroll.osz",
@"1356447 SECONDWALL - Boku wa Boku de shika Nakute.osz",
@"1368054 SECONDWALL - Shooting Star.osz",
@"1398580 La priere - Senjou no Utahime.osz",
@"1403962 m108 - Sunflower.osz",
@"1405913 fiend - FEVER DREAM (feat. yzzyx).osz",
@"1409184 Omoi - Hey William (New Translation).osz",
@"1413418 URBANGARDE - KAMING OUT (Cut Ver.).osz",
@"1417793 P4koo (NONE) - Sogaikan Utopia.osz",
@"1428384 DUAL ALTER WORLD - Veracila.osz",
@"1442963 PUP - DVP.osz",
@"1460370 Sound Souler - Empty Stars.osz",
@"1485184 Koven - Love Wins Again.osz",
@"1496811 T & Sugah - Wicked Days (Cut Ver.).osz",
@"1501511 Masahiro ""Godspeed"" Aoki - Frostbite (Cut Ver.).osz",
@"1511518 T & Sugah X Zazu - Lost On My Own (Cut Ver.).osz",
@"1516617 wotoha - Digital Life Hacker.osz",
@"1524273 Michael Cera Palin - Admiral.osz",
@"1564234 P4koo - Fly High (feat. rerone).osz",
@"1572918 Lexurus - Take Me Away (Cut Ver.).osz",
@"1577313 Kurubukko - The 84th Flight.osz",
@"1587839 Amidst - Droplet.osz",
@"1595193 BlackY - Sakura Ranman Cleopatra.osz",
@"1667560 xi - FREEDOM DiVE.osz",
@"1668789 City Girl - L2M (feat. Kelsey Kuan).osz",
@"1672934 xi - Parousia.osz",
@"1673457 Boom Kitty - Any Other Way (feat. Ivy Marie).osz",
@"1685122 xi - Time files.osz",
@"1689372 NIWASHI - Y.osz",
@"1729551 JOYLESS - Dream.osz",
@"1742868 Ritorikal - Synergy.osz",
@"1757511 KINEMA106 - KARASU.osz",
@"1778169 Ricky Montgomery - Cabo.osz",
@"1848184 FRASER EDWARDS - Ruination.osz",
@"1862574 Pegboard Nerds - Try This (Cut Ver.).osz",
@"1873680 happy30 - You spin my world.osz",
@"1890055 A.SAKA - Mutsuki Akari no Yuki.osz",
@"1911933 Marmalade butcher - Waltz for Chroma (feat. Natsushiro Takaaki).osz",
@"1940007 Mili - Ga1ahad and Scientific Witchery.osz",
@"1948970 Shadren - You're Here Forever.osz",
@"1967856 Annabel - alpine blue.osz",
@"1969316 Silentroom - NULCTRL.osz",
@"1978614 Krimek - Idyllic World.osz",
@"1991315 Feint - Tower Of Heaven (You Are Slaves) (Cut Ver.).osz",
@"1997470 tephe - Genjitsu Escape.osz",
@"1999116 soowamisu - .vaporcore.osz",
@"2010589 Junk - Yellow Smile (bms edit).osz",
@"2022054 Yokomin - STINGER.osz",
@"2025686 Aice room - For U.osz",
@"2035357 C-Show feat. Ishizawa Yukari - Border Line.osz",
@"2039403 SECONDWALL - Freedom.osz",
@"2046487 Rameses B - Against the Grain (feat. Veela).osz",
@"2052201 ColBreakz & Vizzen - Remember.osz",
@"2055535 Sephid - Thunderstrike 1988.osz",
@"2057584 SAMString - Ataraxia.osz",
@"2067270 Blue Stahli - The Fall.osz",
@"2075039 garlagan - Skyless.osz",
@"2079089 Hamu feat. yuiko - Innocent Letter.osz",
@"2082895 FATE GEAR - Heart's Grave.osz",
@"2085974 HoneyComeBear - Twilight.osz",
@"2094934 F.O.O.L & Laura Brehm - Waking Up.osz",
@"2097481 Mameyudoufu - Wave feat. Aitsuki Nakuru.osz",
@"2106075 MYUKKE. - The 89's Momentum.osz",
@"2117392 t+pazolite & Komiya Mao - Elustametat.osz",
@"2123533 LeaF - Calamity Fortune.osz",
@"2143876 Alkome - Your Voice.osz",
@"2145826 Sephid - Cross-D Skyline.osz",
@"2153172 Emiru no Aishita Tsukiyo ni Dai San Gensou Kyoku wo - Eternal Bliss.osz",
};
private static readonly string[] bundled_taiko =
{
"707824 Fractal Dreamers - Fortuna Redux.osz",
"789553 Cranky - Ran.osz",
"827822 Function Phantom - Neuronecia.osz",
"847323 Nakanojojo - Bittersweet (feat. Kuishinboakachan a.k.a Kiato).osz",
"847433 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI.osz",
"847576 dark cat - hot chocolate.osz",
"847957 Wisp X - Final Moments.osz",
"876282 VINXIS - Greetings.osz",
"876648 Thaehan - Angry Birds Epic (Remix).osz",
"877069 IAHN - Transform (Original Mix).osz",
"877496 Thaehan - Leprechaun.osz",
"877935 Thaehan - Overpowered.osz",
"878344 yuki. - Be Your Light.osz",
"918446 VINXIS - Facade.osz",
"918903 LukHash - Ghosts.osz",
"919251 *namirin - Hitokoto no Kyori.osz",
"919704 S3RL - I Will Pick You Up (feat. Tamika).osz",
"921535 SOOOO - Raven Haven.osz",
"927206 *namirin - Kanzen Shouri*Esper Girl.osz",
"927544 Camellia feat. Nanahira - Kansoku Eisei.osz",
"930806 Nakanojojo - Pararara (feat. Amekoya).osz",
"931741 Camellia - Quaoar.osz",
"935699 Rin - Mythic set ~ Heart-Stirring Urban Legends.osz",
"935732 Thaehan - Yuujou.osz",
"941145 Function Phantom - Euclid.osz",
"942334 Dictate - Cauldron.osz",
"946540 nanobii - astral blast.osz",
"948844 Rin - Kishinjou set 01 ~ Mist Lake.osz",
"949122 Wisp X - Petal.osz",
"951618 Rin - Kishinjou set 02 ~ Mermaid from the Uncharted Land.osz",
"957412 Rin - Lunatic set 16 ~ The Space Shrine Maiden Returns Home.osz",
"961335 Thaehan - Insert Coin.osz",
"965178 The Flashbulb - DIDJ PVC.osz",
"966087 The Flashbulb - Creep.osz",
"966277 The Flashbulb - Amen Iraq.osz",
"966407 LukHash - ROOM 12.osz",
"966451 The Flashbulb - Six Acid Strings.osz",
"972301 BilliumMoto - four veiled stars.osz",
"973173 nanobii - popsicle beach.osz",
"973954 BilliumMoto - Rocky Buinne (Short Ver.).osz",
"975435 BilliumMoto - life flashes before weeb eyes.osz",
"978759 L. V. Beethoven - Moonlight Sonata (Cranky Remix).osz",
"982559 BilliumMoto - HDHR.osz",
"984361 The Flashbulb - Ninedump.osz",
"1023681 Inferi - The Ruin of Mankind.osz",
"1034358 ALEPH - The Evil Spirit.osz",
"1037567 ALEPH - Scintillations.osz",
"1048153 Chroma - [@__@].osz",
"1229307 Venetian Snares - Shaky Sometimes.osz",
"1236083 meganeko - Sirius A (osu! edit).osz",
"1248594 Noisia - Anomaly.osz",
"1272851 siqlo - One Way Street.osz",
"1290736 Kola Kid - good old times.osz",
"1318825 SECONDWALL - Light.osz",
"1320872 MYUKKE. - The 89's Momentum.osz",
"1337389 cute girls doing cute things - Main Heroine.osz",
"1397782 Reku Mochizuki - Yorixiro.osz",
"1407228 II-L - VANGUARD-1.osz",
"1422686 II-L - VANGUARD-2.osz",
"1429217 Street - Phi.osz",
"1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz",
"1447478 Cres. - End Time.osz",
"1449942 m108 - Crescent Sakura.osz",
"1463778 MuryokuP - A tree without a branch.osz",
"1465152 fiend - Fever Dream (feat. yzzyx).osz",
"1472397 MYUKKE. - Boudica.osz",
"1488148 Aoi vs. siqlo - Hacktivism.osz",
"1522733 wotoha - Digital Life Hacker.osz",
"1540010 Marmalade butcher - Floccinaucinihilipilification.osz",
"1584690 MYUKKE. - AKKERA-COUNTRY-BOY.osz",
"1608857 BLOOD STAIN CHILD - S.O.P.H.I.A.osz",
"1609365 Reku Mochizuki - Faith of Eastward.osz",
"1622545 METAROOM - I - DINKI THE STARGUIDE.osz",
"1629336 METAROOM - PINK ORIGINS.osz",
"1644680 Neko Hacker - Pictures feat. 4s4ki.osz",
"1650835 RiraN - Ready For The Madness.osz",
"1661508 PTB10 - Starfall.osz",
"1671987 xi - World Fragments II.osz",
"1703065 tokiwa - wasurena feat. Sennzai.osz",
"1703527 tokiwa feat. Nakamura Sanso - Kotodama Refrain.osz",
"1704340 A-One feat. Shihori - Magic Girl !!.osz",
"1712783 xi - Parousia.osz",
"1718774 Harumaki Gohan - Suisei ni Nareta nara.osz",
"1719687 EmoCosine - Love Kills U.osz",
"1733940 WHITEFISTS feat. Sennzai - Paralyzed Ash.osz",
"1734692 EmoCosine - Cutter.osz",
"1739529 luvlxckdown - tbh i dont like being social.osz",
"1756970 Kurubukko vs. yukitani - Minamichita EVOLVED.osz",
"1762209 Marmalade butcher - Immortality Math Club.osz",
"1765720 ZxNX - FORTALiCE.osz",
"1786165 NILFRUITS - Arandano.osz",
"1787258 SAMString - Night Fighter.osz",
"1791462 ZxNX - Schadenfreude.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 =
{
"554256 Helblinde - When Time Sleeps.osz",
"693123 yuki. - Nadeshiko Sensation.osz",
"767009 OISHII - PIZZA PLAZA.osz",
"767346 Thaehan - Bwa !.osz",
"815162 VINXIS - Greetings.osz",
"840964 cYsmix - Breeze.osz",
"932657 Wisp X - Eventide.osz",
"933700 onumi - CONFUSION PART ONE.osz",
"933984 onumi - PERSONALITY.osz",
"934785 onumi - FAKE.osz",
"936545 onumi - REGRET PART ONE.osz",
"943803 Fractal Dreamers - Everything for a Dream.osz",
"943876 S3RL - I Will Pick You Up (feat. Tamika).osz",
"946773 Trial & Error - DREAMING COLOR (Short Ver.).osz",
"955808 Trial & Error - Tokoyami no keiyaku KEGARETA-SHOUJO feat. GUMI (Short Ver.).osz",
"957808 Fractal Dreamers - Module_410.osz",
"957842 antiPLUR - One Life Left to Live.osz",
"965730 The Flashbulb - Lawn Wake IV (Black).osz",
"966240 Creo - Challenger.osz",
"968232 Rin - Lunatic set 15 ~ The Moon as Seen from the Shrine.osz",
"972302 VINXIS - A Centralized View.osz",
"972887 HyuN - Illusion of Inflict.osz",
"1008600 LukHash - WHEN AN ANGEL DIES.osz",
"1032103 LukHash - H8 U.osz",
@"693123 yuki. - Nadeshiko Sensation.osz",
@"833719 FOLiACETATE - Heterochromia Iridis.osz",
@"981762 siromaru + cranky - conflict.osz",
@"1008600 LukHash - WHEN AN ANGEL DIES.osz",
@"1071294 dark cat - pursuit of happiness.osz",
@"1102115 meganeko - Nova.osz",
@"1115500 Chopin - Etude Op. 25, No. 12 (meganeko Remix).osz",
@"1128274 LeaF - Wizdomiot.osz",
@"1141049 HyuN feat. JeeE - Fallen Angel.osz",
@"1148215 Zekk - Fluctuation.osz",
@"1151833 ginkiha - nightfall.osz",
@"1158124 PUP - Dark Days.osz",
@"1184890 IAHN - Transform (Original Mix).osz",
@"1195922 Disasterpeace - Home.osz",
@"1197461 MIMI - Nanimo nai Youna.osz",
@"1197924 Camellia feat. Nanahira - Looking For A New Adventure.osz",
@"1203594 ginkiha - Anemoi.osz",
@"1211572 MIMI - Lapis Lazuli.osz",
@"1231601 Lime - Harmony.osz",
@"1240162 P4koo - 8th:Planet ~Re:search~.osz",
@"1246000 Zekk - Calling.osz",
@"1249928 Thaehan - Yuujou.osz",
@"1258751 Umeboshi Chazuke - ICHIBANBOSHI*ROCKET.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 =
{
"943516 antiPLUR - Clockwork Spooks.osz",
"946394 VINXIS - Three Times The Original Charm.osz",
"966408 antiPLUR - One Life Left to Live.osz",
"971561 antiPLUR - Runengon.osz",
"983864 James Landino - Shiba Island.osz",
"989512 BilliumMoto - 1xMISS.osz",
"994104 James Landino - Reaction feat. Slyleaf.osz",
"1003217 nekodex - circles!.osz",
"1009907 James Landino & Kabuki - Birdsong.osz",
"1015169 Thaehan - Insert Coin.osz",
@"1008419 BilliumMoto - Four Veiled Stars.osz",
@"1025170 Frums - We Want To Run.osz",
@"1092856 F-777 - Viking Arena.osz",
@"1139247 O2i3 - Heart Function.osz",
@"1154007 LeaF - ATHAZA.osz",
@"1170054 Zekk - Fallen.osz",
@"1212132 Street - Koiyamai (TV Size).osz",
@"1226466 Se-U-Ra - Elif to Shiro Kura no Yoru -Called-.osz",
@"1247210 Frums - Credits.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)
{
if (model == null)
return Empty();
// prefer online cover where available.
if (model?.BeatmapSet is IBeatmapSetOnlineInfo online)
if (model.BeatmapSet is IBeatmapSetOnlineInfo online)
return new OnlineBeatmapSetCover(online, beatmapSetCoverType);
if (model is BeatmapInfo localModel)

View File

@ -27,8 +27,17 @@ namespace osu.Game.Beatmaps.Drawables
set => base.Masking = value;
}
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
protected override double LoadDelay { get; }
private readonly double timeBeforeUnload;
protected override double TransformDuration => 400;
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover, double timeBeforeLoad = 500, double timeBeforeUnload = 1000)
{
LoadDelay = timeBeforeLoad;
this.timeBeforeUnload = timeBeforeUnload;
this.coverType = coverType;
InternalChild = new Box
@ -38,12 +47,12 @@ namespace osu.Game.Beatmaps.Drawables
};
}
protected override double LoadDelay => 500;
protected override double TransformDuration => 400;
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, timeBeforeUnload)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
{

View File

@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps
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();

View File

@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Formats
base.ParseStreamInto(stream, beatmap);
applyDifficultyRestrictions(beatmap.Difficulty, beatmap);
flushPendingPoints();
// Objects may be out of order *only* if a user has manually edited an .osu file.
@ -102,10 +104,30 @@ namespace osu.Game.Beatmaps.Formats
}
}
/// <summary>
/// Ensures that all <see cref="BeatmapDifficulty"/> settings are within the allowed ranges.
/// See also: https://github.com/peppy/osu-stable-reference/blob/0e425c0d525ef21353c8293c235cc0621d28338b/osu!/GameplayElements/Beatmaps/Beatmap.cs#L567-L614
/// </summary>
private static void applyDifficultyRestrictions(BeatmapDifficulty difficulty, Beatmap beatmap)
{
difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10);
// mania uses "circle size" for key count, thus different allowable range
difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3
? Math.Clamp(difficulty.CircleSize, 0, 10)
: Math.Clamp(difficulty.CircleSize, 1, 18);
difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10);
difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10);
difficulty.SliderMultiplier = Math.Clamp(difficulty.SliderMultiplier, 0.4, 3.6);
difficulty.SliderTickRate = Math.Clamp(difficulty.SliderTickRate, 0.5, 8);
}
/// <summary>
/// Processes the beatmap such that a new combo is started the first hitobject following each break.
/// </summary>
private void postProcessBreaks(Beatmap beatmap)
private static void postProcessBreaks(Beatmap beatmap)
{
int currentBreak = 0;
bool forceNewCombo = false;
@ -161,14 +183,12 @@ namespace osu.Game.Beatmaps.Formats
/// This method's intention is to restore those legacy defaults.
/// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29
/// </summary>
private void applyLegacyDefaults(BeatmapInfo beatmapInfo)
private static void applyLegacyDefaults(BeatmapInfo beatmapInfo)
{
beatmapInfo.WidescreenStoryboard = 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)
{
switch (section)
@ -404,11 +424,11 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"SliderMultiplier":
difficulty.SliderMultiplier = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.4, 3.6);
difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value);
break;
case @"SliderTickRate":
difficulty.SliderTickRate = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.5, 8);
difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value);
break;
}
}
@ -417,43 +437,57 @@ namespace osu.Game.Beatmaps.Formats
{
string[] split = line.Split(',');
if (!Enum.TryParse(split[0], out LegacyEventType type))
throw new InvalidDataException($@"Unknown event type: {split[0]}");
// Until we have full storyboard encoder coverage, let's track any lines which aren't handled
// and store them to a temporary location such that they aren't lost on editor save / export.
bool lineSupportedByEncoder = false;
switch (type)
if (Enum.TryParse(split[0], out LegacyEventType type))
{
case LegacyEventType.Sprite:
// Generally, the background is the first thing defined in a beatmap file.
// 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.
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
break;
switch (type)
{
case LegacyEventType.Sprite:
// Generally, the background is the first thing defined in a beatmap file.
// 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.
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
lineSupportedByEncoder = true;
}
case LegacyEventType.Video:
string filename = CleanFilename(split[2]);
break;
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match.
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
}
case LegacyEventType.Video:
string filename = CleanFilename(split[2]);
break;
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match.
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
lineSupportedByEncoder = true;
}
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break;
break;
case LegacyEventType.Break:
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
lineSupportedByEncoder = true;
break;
beatmap.Breaks.Add(new BreakPeriod(start, end));
break;
case LegacyEventType.Break:
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
beatmap.Breaks.Add(new BreakPeriod(start, end));
lineSupportedByEncoder = true;
break;
}
}
if (!lineSupportedByEncoder)
beatmap.UnhandledEventLines.Add(line);
}
private void handleTimingPoint(string line)

View File

@ -156,6 +156,9 @@ namespace osu.Game.Beatmaps.Formats
foreach (var b in beatmap.Breaks)
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)

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Commands;
using osuTK;
using osuTK.Graphics;
@ -17,7 +18,7 @@ namespace osu.Game.Beatmaps.Formats
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
{
private StoryboardSprite? storyboardSprite;
private CommandTimelineGroup? timelineGroup;
private StoryboardCommandGroup? currentCommandsGroup;
private Storyboard storyboard = null!;
@ -164,7 +165,7 @@ namespace osu.Game.Beatmaps.Formats
else
{
if (depth < 2)
timelineGroup = storyboardSprite?.TimelineGroup;
currentCommandsGroup = storyboardSprite?.Commands;
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 endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue;
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;
}
@ -184,7 +185,7 @@ namespace osu.Game.Beatmaps.Formats
{
double startTime = Parsing.ParseDouble(split[1]);
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;
}
@ -203,7 +204,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
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;
}
@ -211,7 +212,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
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;
}
@ -221,7 +222,7 @@ namespace osu.Game.Beatmaps.Formats
float startY = Parsing.ParseFloat(split[5]);
float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX;
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;
}
@ -229,7 +230,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
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;
}
@ -239,8 +240,8 @@ namespace osu.Game.Beatmaps.Formats
float startY = Parsing.ParseFloat(split[5]);
float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX;
float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY;
timelineGroup?.X.Add(easing, startTime, endTime, startX, endX);
timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY);
currentCommandsGroup?.AddX(easing, startTime, endTime, startX, endX);
currentCommandsGroup?.AddY(easing, startTime, endTime, startY, endY);
break;
}
@ -248,7 +249,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
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;
}
@ -256,7 +257,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
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;
}
@ -268,7 +269,7 @@ namespace osu.Game.Beatmaps.Formats
float endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed;
float endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen;
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(endRed / 255f, endGreen / 255f, endBlue / 255f, 1));
break;
@ -281,16 +282,16 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
case "A":
timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive,
currentCommandsGroup?.AddBlendingParameters(easing, startTime, endTime, BlendingParameters.Additive,
startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
break;
case "H":
timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime);
currentCommandsGroup?.AddFlipH(easing, startTime, endTime, true, startTime == endTime);
break;
case "V":
timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime);
currentCommandsGroup?.AddFlipV(easing, startTime, endTime, true, startTime == endTime);
break;
}

View File

@ -42,6 +42,12 @@ namespace osu.Game.Beatmaps
/// </summary>
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>
/// Total amount of break time in the beatmap.
/// </summary>

View File

@ -44,11 +44,34 @@ namespace osu.Game.Beatmaps
this.storage = storage;
// avoid downloading / using cache for unit tests.
if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name))
if (shouldFetchCache())
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 =>
// no download in progress.
cacheDownloadRequest == null
@ -124,6 +147,8 @@ namespace osu.Game.Beatmaps
private void prepareLocalCache()
{
bool isRefetch = storage.Exists(cache_database_name);
string cacheFilePath = storage.GetFullPath(cache_database_name);
string compressedCacheFilePath = $@"{cacheFilePath}.bz2";
@ -132,9 +157,15 @@ namespace osu.Game.Beatmaps
cacheDownloadRequest.Failed += ex =>
{
File.Delete(compressedCacheFilePath);
File.Delete(cacheFilePath);
Logger.Log($@"{nameof(BeatmapUpdaterMetadataLookup)}'s online cache download failed: {ex}", LoggingTarget.Database);
// 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);
log($@"Online cache download failed: {ex}");
};
cacheDownloadRequest.Finished += () =>
@ -143,15 +174,22 @@ namespace osu.Game.Beatmaps
{
using (var stream = File.OpenRead(cacheDownloadRequest.Filename))
using (var outStream = File.OpenWrite(cacheFilePath))
using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
bz2.CopyTo(outStream);
{
// ensure to clobber any and all existing data to avoid accidental corruption.
outStream.SetLength(0);
using (var bz2 = new BZip2Stream(stream, CompressionMode.Decompress, false))
bz2.CopyTo(outStream);
}
// set to null on completion to allow lookups to begin using the new source
cacheDownloadRequest = null;
log(@"Local cache fetch completed successfully.");
}
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);
}
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) =>
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}");

View File

@ -202,7 +202,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader]
private void load()
{
AddInternal(addOrRemoveButton = new IconButton
AddInternal(addOrRemoveButton = new NoFocusChangeIconButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@ -271,6 +271,11 @@ namespace osu.Game.Collections
}
protected override Drawable CreateContent() => (Content)base.CreateContent();
private partial class NoFocusChangeIconButton : IconButton
{
public override bool ChangeFocusOnClick => false;
}
}
}
}

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