mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 15:47:26 +08:00
Merge branch 'master' into Save-Score-Failed
This commit is contained in:
commit
f3a6e646a6
@ -16,3 +16,6 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
|
||||
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
|
||||
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
|
||||
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
|
||||
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!-- Contains required properties for osu!framework projects. -->
|
||||
<Project>
|
||||
<PropertyGroup Label="C#">
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
60
Gemfile.lock
60
Gemfile.lock
@ -8,20 +8,20 @@ GEM
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.570.0)
|
||||
aws-sdk-core (3.130.0)
|
||||
aws-partitions (1.601.0)
|
||||
aws-sdk-core (3.131.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.55.0)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.57.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.113.0)
|
||||
aws-sdk-s3 (1.114.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-sigv4 (1.5.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
@ -36,7 +36,7 @@ GEM
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.92.1)
|
||||
excon (0.92.3)
|
||||
faraday (1.10.0)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
@ -56,8 +56,8 @@ GEM
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
@ -66,7 +66,7 @@ GEM
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.205.1)
|
||||
fastlane (2.206.2)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@ -110,9 +110,9 @@ GEM
|
||||
souyuz (= 0.11.1)
|
||||
fastlane-plugin-xamarin (0.6.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.16.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-core (0.4.2)
|
||||
google-apis-androidpublisher_v3 (0.23.0)
|
||||
google-apis-core (>= 0.6, < 2.a)
|
||||
google-apis-core (0.6.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@ -121,19 +121,19 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.10.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.11.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.12.0)
|
||||
google-apis-core (>= 0.6, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.9.0)
|
||||
google-apis-core (>= 0.6, < 2.a)
|
||||
google-apis-storage_v1 (0.16.0)
|
||||
google-apis-core (>= 0.6, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.36.1)
|
||||
google-cloud-storage (1.36.2)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
@ -141,7 +141,7 @@ GEM
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.1.2)
|
||||
googleauth (1.2.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@ -149,12 +149,12 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.4)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.1)
|
||||
jwt (2.3.0)
|
||||
json (2.6.2)
|
||||
jwt (2.4.1)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
@ -169,10 +169,10 @@ GEM
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
public_suffix (4.0.6)
|
||||
public_suffix (4.0.7)
|
||||
racc (1.6.0)
|
||||
rake (13.0.6)
|
||||
representable (3.1.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
@ -182,9 +182,9 @@ GEM
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.16.1)
|
||||
signet (0.17.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.0)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
@ -205,11 +205,11 @@ GEM
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.1)
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.21.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
|
@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.616.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.615.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.707.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
97
osu.Android/AndroidMouseSettings.cs
Normal file
97
osu.Android/AndroidMouseSettings.cs
Normal file
@ -0,0 +1,97 @@
|
||||
// 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 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.DisableMouseButtons,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,11 @@ using System;
|
||||
using Android.App;
|
||||
using Android.OS;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Android.Input;
|
||||
using osu.Framework.Input.Handlers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
using Xamarin.Essentials;
|
||||
@ -75,10 +79,28 @@ namespace osu.Android
|
||||
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
|
||||
}
|
||||
|
||||
public override void SetHost(GameHost host)
|
||||
{
|
||||
base.SetHost(host);
|
||||
host.Window.CursorState |= CursorState.Hidden;
|
||||
}
|
||||
|
||||
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
|
||||
|
||||
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
|
||||
{
|
||||
switch (handler)
|
||||
{
|
||||
case AndroidMouseHandler mh:
|
||||
return new AndroidMouseSettings(mh);
|
||||
|
||||
default:
|
||||
return base.CreateSettingsSubsectionFor(handler);
|
||||
}
|
||||
}
|
||||
|
||||
private class AndroidBatteryInfo : BatteryInfo
|
||||
{
|
||||
public override double ChargeLevel => Battery.ChargeLevel;
|
||||
|
@ -26,6 +26,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AndroidMouseSettings.cs" />
|
||||
<Compile Include="GameplayScreenRotationLocker.cs" />
|
||||
<Compile Include="OsuGameActivity.cs" />
|
||||
<Compile Include="OsuGameAndroid.cs" />
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Desktop
|
||||
client.OnReady += onReady;
|
||||
|
||||
// safety measure for now, until we performance test / improve backoff for failed connections.
|
||||
client.OnConnectionFailed += (_, __) => client.Deinitialize();
|
||||
client.OnConnectionFailed += (_, _) => client.Deinitialize();
|
||||
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
|
||||
|
||||
|
@ -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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@ -20,25 +18,34 @@ using osu.Framework;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Updater;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework.Input.Handlers;
|
||||
using osu.Framework.Input.Handlers.Joystick;
|
||||
using osu.Framework.Input.Handlers.Mouse;
|
||||
using osu.Framework.Input.Handlers.Tablet;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.Settings.Sections.Input;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
internal class OsuGameDesktop : OsuGame
|
||||
{
|
||||
public OsuGameDesktop(string[] args = null)
|
||||
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
|
||||
|
||||
public OsuGameDesktop(string[]? args = null)
|
||||
: base(args)
|
||||
{
|
||||
}
|
||||
|
||||
public override StableStorage GetStorageForStableInstall()
|
||||
public override StableStorage? GetStorageForStableInstall()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Host is DesktopGameHost desktopHost)
|
||||
{
|
||||
string stablePath = getStableInstallPath();
|
||||
string? stablePath = getStableInstallPath();
|
||||
if (!string.IsNullOrEmpty(stablePath))
|
||||
return new StableStorage(stablePath, desktopHost);
|
||||
}
|
||||
@ -51,11 +58,11 @@ namespace osu.Desktop
|
||||
return null;
|
||||
}
|
||||
|
||||
private string getStableInstallPath()
|
||||
private string? getStableInstallPath()
|
||||
{
|
||||
static bool checkExists(string p) => Directory.Exists(Path.Combine(p, "Songs")) || File.Exists(Path.Combine(p, "osu!.cfg"));
|
||||
|
||||
string stableInstallPath;
|
||||
string? stableInstallPath;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
@ -83,15 +90,15 @@ namespace osu.Desktop
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private string getStableInstallPathFromRegistry()
|
||||
private string? getStableInstallPathFromRegistry()
|
||||
{
|
||||
using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu"))
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
|
||||
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
}
|
||||
|
||||
protected override UpdateManager CreateUpdateManager()
|
||||
{
|
||||
string packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER");
|
||||
string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER");
|
||||
|
||||
if (!string.IsNullOrEmpty(packageManaged))
|
||||
return new NoActionUpdateManager();
|
||||
@ -118,6 +125,8 @@ namespace osu.Desktop
|
||||
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
|
||||
|
||||
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
|
||||
|
||||
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
|
||||
}
|
||||
|
||||
public override void SetHost(GameHost host)
|
||||
@ -134,8 +143,26 @@ namespace osu.Desktop
|
||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||
}
|
||||
|
||||
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
|
||||
{
|
||||
switch (handler)
|
||||
{
|
||||
case ITabletHandler th:
|
||||
return new TabletSettings(th);
|
||||
|
||||
case MouseHandler mh:
|
||||
return new MouseSettings(mh);
|
||||
|
||||
case JoystickHandler jh:
|
||||
return new JoystickSettings(jh);
|
||||
|
||||
default:
|
||||
return base.CreateSettingsSubsectionFor(handler);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<string> importableFiles = new List<string>();
|
||||
private ScheduledDelegate importSchedule;
|
||||
private ScheduledDelegate? importSchedule;
|
||||
|
||||
private void fileDrop(string[] filePaths)
|
||||
{
|
||||
@ -168,5 +195,11 @@ namespace osu.Desktop
|
||||
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
osuSchemeLinkIPCChannel?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Tournament;
|
||||
using Squirrel;
|
||||
@ -65,19 +66,8 @@ namespace osu.Desktop
|
||||
{
|
||||
if (!host.IsPrimaryInstance)
|
||||
{
|
||||
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
|
||||
{
|
||||
var importer = new ArchiveImportIPCChannel(host);
|
||||
|
||||
foreach (string file in args)
|
||||
{
|
||||
Console.WriteLine(@"Importing {0}", file);
|
||||
if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
|
||||
throw new TimeoutException(@"IPC took too long to send");
|
||||
}
|
||||
|
||||
if (trySendIPCMessage(host, cwd, args))
|
||||
return;
|
||||
}
|
||||
|
||||
// we want to allow multiple instances to be started when in debug.
|
||||
if (!DebugUtils.IsDebugBuild)
|
||||
@ -108,21 +98,49 @@ namespace osu.Desktop
|
||||
}
|
||||
}
|
||||
|
||||
private static bool trySendIPCMessage(IIpcHost host, string cwd, string[] args)
|
||||
{
|
||||
if (args.Length == 1 && args[0].StartsWith(OsuGameBase.OSU_PROTOCOL, StringComparison.Ordinal))
|
||||
{
|
||||
var osuSchemeLinkHandler = new OsuSchemeLinkIPCChannel(host);
|
||||
if (!osuSchemeLinkHandler.HandleLinkAsync(args[0]).Wait(3000))
|
||||
throw new IPCTimeoutException(osuSchemeLinkHandler.GetType());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
|
||||
{
|
||||
var importer = new ArchiveImportIPCChannel(host);
|
||||
|
||||
foreach (string file in args)
|
||||
{
|
||||
Console.WriteLine(@"Importing {0}", file);
|
||||
if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
|
||||
throw new IPCTimeoutException(importer.GetType());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void setupSquirrel()
|
||||
{
|
||||
SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) =>
|
||||
SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
|
||||
{
|
||||
tools.CreateShortcutForThisExe();
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
}, onAppUpdate: (version, tools) =>
|
||||
}, onAppUpdate: (_, tools) =>
|
||||
{
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
}, onAppUninstall: (version, tools) =>
|
||||
}, onAppUninstall: (_, tools) =>
|
||||
{
|
||||
tools.RemoveShortcutForThisExe();
|
||||
tools.RemoveUninstallerRegistryEntry();
|
||||
}, onEveryRun: (version, tools, firstRun) =>
|
||||
}, onEveryRun: (_, _, _) =>
|
||||
{
|
||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
||||
// causes the right-click context menu to function incorrectly.
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Benchmarks
|
||||
|
||||
realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME);
|
||||
|
||||
realm.Run(r =>
|
||||
realm.Run(_ =>
|
||||
{
|
||||
realm.Write(c => c.Add(TestResources.CreateTestBeatmapSetInfo(rulesets: new[] { new OsuRuleset().RulesetInfo })));
|
||||
});
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Benchmarks
|
||||
}
|
||||
});
|
||||
|
||||
done.Wait();
|
||||
done.Wait(60000);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
@ -115,7 +115,7 @@ namespace osu.Game.Benchmarks
|
||||
}
|
||||
});
|
||||
|
||||
done.Wait();
|
||||
done.Wait(60000);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
|
@ -24,21 +24,24 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } },
|
||||
new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } },
|
||||
new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } },
|
||||
new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } },
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } },
|
||||
new object[] { LegacyMods.Perfect, new[] { typeof(CatchModPerfect) } },
|
||||
new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } }
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(catch_mod_mapping))]
|
||||
[TestCase(LegacyMods.Cinema, new[] { typeof(CatchModCinema) })]
|
||||
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
|
||||
[TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })]
|
||||
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
|
||||
[TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })]
|
||||
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
|
||||
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
|
||||
|
||||
[TestCaseSource(nameof(catch_mod_mapping))]
|
||||
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })]
|
||||
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })]
|
||||
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })]
|
||||
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
|
||||
|
||||
[TestCaseSource(nameof(catch_mod_mapping))]
|
||||
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
|
||||
|
||||
protected override Ruleset CreateRuleset() => new CatchRuleset();
|
||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
new JuiceStreamPathVertex(20, -5)
|
||||
}));
|
||||
|
||||
removeCount = path.RemoveVertices((_, i) => true);
|
||||
removeCount = path.RemoveVertices((_, _) => true);
|
||||
Assert.That(removeCount, Is.EqualTo(1));
|
||||
Assert.That(path.Vertices, Is.EqualTo(new[]
|
||||
{
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
|
||||
AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
||||
AddStep("exit player", () => Player.Exit());
|
||||
CreateTest(null);
|
||||
CreateTest();
|
||||
}
|
||||
|
||||
AddAssert("legacy HUD combo counter hidden", () =>
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
hyperDashCount = 0;
|
||||
|
||||
// this needs to be done within the frame stable context due to how quickly hyperdash state changes occur.
|
||||
Player.DrawableRuleset.FrameStableComponents.OnUpdate += d =>
|
||||
Player.DrawableRuleset.FrameStableComponents.OnUpdate += _ =>
|
||||
{
|
||||
var catcher = Player.ChildrenOfType<Catcher>().FirstOrDefault();
|
||||
|
||||
|
@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
protected override string RulesetPrefix => "catch"; // todo: use CatchRuleset.SHORT_NAME;
|
||||
|
||||
protected override string ComponentName => Component.ToString().ToLower();
|
||||
protected override string ComponentName => Component.ToString().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
@ -31,9 +30,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values)
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
{
|
||||
base.FromDatabaseAttributes(values);
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_AIM];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
|
@ -135,15 +135,15 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
switch (BlueprintContainer.CurrentTool)
|
||||
{
|
||||
case SelectTool _:
|
||||
case SelectTool:
|
||||
if (EditorBeatmap.SelectedHitObjects.Count == 0)
|
||||
return null;
|
||||
|
||||
double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime);
|
||||
return getLastSnappableHitObject(minTime);
|
||||
|
||||
case FruitCompositionTool _:
|
||||
case JuiceStreamCompositionTool _:
|
||||
case FruitCompositionTool:
|
||||
case JuiceStreamCompositionTool:
|
||||
if (!CursorInPlacementArea)
|
||||
return null;
|
||||
|
||||
|
@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
case Droplet droplet:
|
||||
return droplet is TinyDroplet ? PositionRange.EMPTY : new PositionRange(droplet.OriginalX);
|
||||
|
||||
case JuiceStream _:
|
||||
case JuiceStream:
|
||||
return GetPositionRange(hitObject.NestedHitObjects);
|
||||
|
||||
case BananaShower _:
|
||||
case BananaShower:
|
||||
// A banana shower occupies the whole screen width.
|
||||
return new PositionRange(0, CatchPlayfield.WIDTH);
|
||||
|
||||
|
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case BananaShower _:
|
||||
case BananaShower:
|
||||
return false;
|
||||
|
||||
case JuiceStream juiceStream:
|
||||
|
@ -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 osu.Framework.Utils;
|
||||
|
@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
|
@ -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.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
{
|
||||
}
|
||||
|
||||
public CatchReplayFrame(double time, float? position = null, bool dashing = false, CatchReplayFrame lastFrame = null)
|
||||
public CatchReplayFrame(double time, float? position = null, bool dashing = false, CatchReplayFrame? lastFrame = null)
|
||||
: base(time)
|
||||
{
|
||||
Position = position ?? -1;
|
||||
@ -40,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
}
|
||||
}
|
||||
|
||||
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
|
||||
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
|
||||
{
|
||||
Position = currentFrame.Position.X;
|
||||
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
|
||||
|
@ -389,13 +389,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case Fruit _:
|
||||
case Fruit:
|
||||
return caughtFruitPool.Get();
|
||||
|
||||
case Banana _:
|
||||
case Banana:
|
||||
return caughtBananaPool.Get();
|
||||
|
||||
case Droplet _:
|
||||
case Droplet:
|
||||
return caughtDropletPool.Get();
|
||||
|
||||
default:
|
||||
|
@ -120,10 +120,10 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
lastHyperDashState = Catcher.HyperDashing;
|
||||
}
|
||||
|
||||
public void SetCatcherPosition(float X)
|
||||
public void SetCatcherPosition(float x)
|
||||
{
|
||||
float lastPosition = Catcher.X;
|
||||
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
|
||||
float newPosition = Math.Clamp(x, 0, CatchPlayfield.WIDTH);
|
||||
|
||||
Catcher.X = newPosition;
|
||||
|
||||
|
@ -23,10 +23,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } },
|
||||
new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } },
|
||||
new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } },
|
||||
new object[] { LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) } },
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } },
|
||||
new object[] { LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) } },
|
||||
new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } },
|
||||
new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } },
|
||||
new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } },
|
||||
@ -34,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } },
|
||||
new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } },
|
||||
new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } },
|
||||
new object[] { LegacyMods.Cinema, new[] { typeof(ManiaModCinema) } },
|
||||
new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } },
|
||||
new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } },
|
||||
new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } },
|
||||
@ -45,12 +42,18 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(mania_mod_mapping))]
|
||||
[TestCase(LegacyMods.Cinema, new[] { typeof(ManiaModCinema) })]
|
||||
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
|
||||
[TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })]
|
||||
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
|
||||
[TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })]
|
||||
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
|
||||
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
|
||||
|
||||
[TestCaseSource(nameof(mania_mod_mapping))]
|
||||
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })]
|
||||
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })]
|
||||
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
|
||||
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
|
||||
|
||||
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
|
@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
switch (original)
|
||||
{
|
||||
case IHasDistance _:
|
||||
case IHasDistance:
|
||||
{
|
||||
var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
|
||||
conversion = generator;
|
||||
|
@ -1,10 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
@ -20,12 +19,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
[JsonProperty("great_hit_window")]
|
||||
public double GreatHitWindow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The score multiplier applied via score-reducing mods.
|
||||
/// </summary>
|
||||
[JsonProperty("score_multiplier")]
|
||||
public double ScoreMultiplier { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
@ -34,17 +27,15 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values)
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
{
|
||||
base.FromDatabaseAttributes(values);
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||
ScoreMultiplier = getScoreMultiplier(mods),
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
|
||||
};
|
||||
}
|
||||
@ -147,32 +146,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private double getScoreMultiplier(Mod[] mods)
|
||||
{
|
||||
double scoreMultiplier = 1;
|
||||
|
||||
foreach (var m in mods)
|
||||
{
|
||||
switch (m)
|
||||
{
|
||||
case ManiaModNoFail _:
|
||||
case ManiaModEasy _:
|
||||
case ManiaModHalfTime _:
|
||||
scoreMultiplier *= 0.5;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var maniaBeatmap = (ManiaBeatmap)Beatmap;
|
||||
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
|
||||
|
||||
if (diff > 0)
|
||||
scoreMultiplier *= 0.9;
|
||||
else if (diff < 0)
|
||||
scoreMultiplier *= 0.9 + 0.04 * diff;
|
||||
|
||||
return scoreMultiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,19 +14,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
[JsonProperty("difficulty")]
|
||||
public double Difficulty { get; set; }
|
||||
|
||||
[JsonProperty("accuracy")]
|
||||
public double Accuracy { get; set; }
|
||||
|
||||
[JsonProperty("scaled_score")]
|
||||
public double ScaledScore { get; set; }
|
||||
|
||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||
{
|
||||
foreach (var attribute in base.GetAttributesForDisplay())
|
||||
yield return attribute;
|
||||
|
||||
yield return new PerformanceDisplayAttribute(nameof(Difficulty), "Difficulty", Difficulty);
|
||||
yield return new PerformanceDisplayAttribute(nameof(Accuracy), "Accuracy", Accuracy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,15 +15,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
public class ManiaPerformanceCalculator : PerformanceCalculator
|
||||
{
|
||||
// Score after being scaled by non-difficulty-increasing mods
|
||||
private double scaledScore;
|
||||
|
||||
private int countPerfect;
|
||||
private int countGreat;
|
||||
private int countGood;
|
||||
private int countOk;
|
||||
private int countMeh;
|
||||
private int countMiss;
|
||||
private double scoreAccuracy;
|
||||
|
||||
public ManiaPerformanceCalculator()
|
||||
: base(new ManiaRuleset())
|
||||
@ -34,82 +32,47 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
var maniaAttributes = (ManiaDifficultyAttributes)attributes;
|
||||
|
||||
scaledScore = score.TotalScore;
|
||||
countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);
|
||||
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
countGood = score.Statistics.GetValueOrDefault(HitResult.Good);
|
||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
|
||||
if (maniaAttributes.ScoreMultiplier > 0)
|
||||
{
|
||||
// Scale score up, so it's comparable to other keymods
|
||||
scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier;
|
||||
}
|
||||
scoreAccuracy = customAccuracy;
|
||||
|
||||
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
|
||||
// The specific number has no intrinsic meaning and can be adjusted as needed.
|
||||
double multiplier = 0.8;
|
||||
double multiplier = 8.0;
|
||||
|
||||
if (score.Mods.Any(m => m is ModNoFail))
|
||||
multiplier *= 0.9;
|
||||
multiplier *= 0.75;
|
||||
if (score.Mods.Any(m => m is ModEasy))
|
||||
multiplier *= 0.5;
|
||||
|
||||
double difficultyValue = computeDifficultyValue(maniaAttributes);
|
||||
double accValue = computeAccuracyValue(difficultyValue, maniaAttributes);
|
||||
double totalValue =
|
||||
Math.Pow(
|
||||
Math.Pow(difficultyValue, 1.1) +
|
||||
Math.Pow(accValue, 1.1), 1.0 / 1.1
|
||||
) * multiplier;
|
||||
double totalValue = difficultyValue * multiplier;
|
||||
|
||||
return new ManiaPerformanceAttributes
|
||||
{
|
||||
Difficulty = difficultyValue,
|
||||
Accuracy = accValue,
|
||||
ScaledScore = scaledScore,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
|
||||
private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
|
||||
{
|
||||
double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
|
||||
|
||||
difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
|
||||
if (scaledScore <= 500000)
|
||||
difficultyValue = 0;
|
||||
else if (scaledScore <= 600000)
|
||||
difficultyValue *= (scaledScore - 500000) / 100000 * 0.3;
|
||||
else if (scaledScore <= 700000)
|
||||
difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
|
||||
else if (scaledScore <= 800000)
|
||||
difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
|
||||
else if (scaledScore <= 900000)
|
||||
difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
|
||||
else
|
||||
difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
|
||||
double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
|
||||
* Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
|
||||
* (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
|
||||
|
||||
return difficultyValue;
|
||||
}
|
||||
|
||||
private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes)
|
||||
{
|
||||
if (attributes.GreatHitWindow <= 0)
|
||||
return 0;
|
||||
|
||||
// Lots of arbitrary values from testing.
|
||||
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
||||
double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667)
|
||||
* difficultyValue
|
||||
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
|
||||
|
||||
return accuracyValue;
|
||||
}
|
||||
|
||||
private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss;
|
||||
|
||||
/// <summary>
|
||||
/// Accuracy used to weight judgements independently from the score's actual accuracy.
|
||||
/// </summary>
|
||||
private double customAccuracy => (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
|
@ -146,56 +146,56 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case ManiaModKey1 _:
|
||||
case ManiaModKey1:
|
||||
value |= LegacyMods.Key1;
|
||||
break;
|
||||
|
||||
case ManiaModKey2 _:
|
||||
case ManiaModKey2:
|
||||
value |= LegacyMods.Key2;
|
||||
break;
|
||||
|
||||
case ManiaModKey3 _:
|
||||
case ManiaModKey3:
|
||||
value |= LegacyMods.Key3;
|
||||
break;
|
||||
|
||||
case ManiaModKey4 _:
|
||||
case ManiaModKey4:
|
||||
value |= LegacyMods.Key4;
|
||||
break;
|
||||
|
||||
case ManiaModKey5 _:
|
||||
case ManiaModKey5:
|
||||
value |= LegacyMods.Key5;
|
||||
break;
|
||||
|
||||
case ManiaModKey6 _:
|
||||
case ManiaModKey6:
|
||||
value |= LegacyMods.Key6;
|
||||
break;
|
||||
|
||||
case ManiaModKey7 _:
|
||||
case ManiaModKey7:
|
||||
value |= LegacyMods.Key7;
|
||||
break;
|
||||
|
||||
case ManiaModKey8 _:
|
||||
case ManiaModKey8:
|
||||
value |= LegacyMods.Key8;
|
||||
break;
|
||||
|
||||
case ManiaModKey9 _:
|
||||
case ManiaModKey9:
|
||||
value |= LegacyMods.Key9;
|
||||
break;
|
||||
|
||||
case ManiaModDualStages _:
|
||||
case ManiaModDualStages:
|
||||
value |= LegacyMods.KeyCoop;
|
||||
break;
|
||||
|
||||
case ManiaModFadeIn _:
|
||||
case ManiaModFadeIn:
|
||||
value |= LegacyMods.FadeIn;
|
||||
value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that.
|
||||
break;
|
||||
|
||||
case ManiaModMirror _:
|
||||
case ManiaModMirror:
|
||||
value |= LegacyMods.Mirror;
|
||||
break;
|
||||
|
||||
case ManiaModRandom _:
|
||||
case ManiaModRandom:
|
||||
value |= LegacyMods.Random;
|
||||
break;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;
|
||||
|
||||
protected override string ComponentName => Component.ToString().ToLower();
|
||||
protected override string ComponentName => Component.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
public enum ManiaSkinComponents
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
var rng = new Random((int)Seed.Value);
|
||||
|
||||
int availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
|
||||
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList();
|
||||
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(_ => rng.Next()).ToList();
|
||||
|
||||
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]);
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -57,11 +56,11 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
{
|
||||
switch (point)
|
||||
{
|
||||
case HitPoint _:
|
||||
case HitPoint:
|
||||
actions.Add(columnActions[point.Column]);
|
||||
break;
|
||||
|
||||
case ReleasePoint _:
|
||||
case ReleasePoint:
|
||||
actions.Remove(columnActions[point.Column]);
|
||||
break;
|
||||
}
|
||||
@ -85,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
}
|
||||
}
|
||||
|
||||
private double calculateReleaseTime(HitObject currentObject, HitObject nextObject)
|
||||
private double calculateReleaseTime(HitObject currentObject, HitObject? nextObject)
|
||||
{
|
||||
double endTime = currentObject.GetEndTime();
|
||||
|
||||
@ -96,10 +95,10 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
bool canDelayKeyUpFully = nextObject == null ||
|
||||
nextObject.StartTime > endTime + RELEASE_DELAY;
|
||||
|
||||
return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.StartTime - endTime) * 0.9);
|
||||
return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.AsNonNull().StartTime - endTime) * 0.9);
|
||||
}
|
||||
|
||||
protected override HitObject GetNextObject(int currentIndex)
|
||||
protected override HitObject? GetNextObject(int currentIndex)
|
||||
{
|
||||
int desiredColumn = Beatmap.HitObjects[currentIndex].Column;
|
||||
|
||||
|
@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
|
@ -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.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
Actions.AddRange(actions);
|
||||
}
|
||||
|
||||
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
|
||||
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
|
||||
{
|
||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
||||
|
||||
|
@ -10,7 +10,6 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
@ -41,10 +40,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
private bool editorComponentsReady => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
|
||||
&& editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true
|
||||
&& editor?.ChildrenOfType<Playfield>().FirstOrDefault()?.IsLoaded == true;
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void TestVelocityChangeSavesCorrectly(bool adjustVelocity)
|
||||
@ -52,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
double? velocity = null;
|
||||
|
||||
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("wait for editor load", () => editorComponentsReady);
|
||||
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
|
||||
|
||||
AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time));
|
||||
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
|
||||
@ -91,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("exit", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("wait for editor load", () => editorComponentsReady);
|
||||
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
|
||||
|
||||
AddStep("seek to slider", () => editorClock.Seek(slider.StartTime));
|
||||
AddAssert("slider has correct velocity", () => slider.Velocity == velocity);
|
||||
|
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
|
||||
|
||||
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
|
||||
.Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName });
|
||||
.Returns((string componentName, WrapMode _, WrapMode _) => new Texture(1, 1) { AssetName = componentName });
|
||||
|
||||
Child = new DependencyProvidingContainer
|
||||
{
|
||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
lastResult = null;
|
||||
|
||||
spinner = nextSpinner;
|
||||
spinner.OnNewResult += (o, result) => lastResult = result;
|
||||
spinner.OnNewResult += (_, result) => lastResult = result;
|
||||
}
|
||||
|
||||
return lastResult?.Type == HitResult.Great;
|
||||
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
return false;
|
||||
|
||||
spinner = nextSpinner;
|
||||
spinner.OnNewResult += (o, result) => results.Add(result);
|
||||
spinner.OnNewResult += (_, result) => results.Add(result);
|
||||
|
||||
results.Clear();
|
||||
}
|
||||
|
@ -25,24 +25,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } },
|
||||
new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } },
|
||||
new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } },
|
||||
new object[] { LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) } },
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } },
|
||||
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
|
||||
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
|
||||
new object[] { LegacyMods.Perfect, new[] { typeof(OsuModPerfect) } },
|
||||
new object[] { LegacyMods.Cinema, new[] { typeof(OsuModCinema) } },
|
||||
new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(osu_mod_mapping))]
|
||||
[TestCase(LegacyMods.Cinema, new[] { typeof(OsuModCinema) })]
|
||||
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
|
||||
[TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })]
|
||||
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
|
||||
[TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })]
|
||||
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
|
||||
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
|
||||
|
||||
[TestCaseSource(nameof(osu_mod_mapping))]
|
||||
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })]
|
||||
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })]
|
||||
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })]
|
||||
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
|
||||
|
||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
@ -93,10 +92,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
|
||||
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
|
||||
AddAssert("ball is white", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.White);
|
||||
AddAssert("ball is white", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.White);
|
||||
|
||||
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
|
||||
AddAssert("ball is red", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.Red);
|
||||
AddAssert("ball is red", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.Red);
|
||||
}
|
||||
|
||||
private Slider prepareObject(Slider slider)
|
||||
|
120
osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs
Normal file
120
osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs
Normal 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneSliderFollowCircleInput : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private List<JudgementResult>? judgementResults;
|
||||
private ScoreAccessibleReplayPlayer? currentPlayer;
|
||||
|
||||
[Test]
|
||||
public void TestMaximumDistanceTrackingWithoutMovement(
|
||||
[Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
|
||||
float circleSize,
|
||||
[Values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
|
||||
double velocity)
|
||||
{
|
||||
const double time_slider_start = 1000;
|
||||
|
||||
float circleRadius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (circleSize - 5) / 5) / 2;
|
||||
float followCircleRadius = circleRadius * 1.2f;
|
||||
|
||||
performTest(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = time_slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity },
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(followCircleRadius, 0),
|
||||
}, followCircleRadius),
|
||||
},
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = circleSize,
|
||||
SliderTickRate = 1
|
||||
},
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
}, new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Position = new Vector2(-circleRadius + 1, 0), Actions = { OsuAction.LeftButton }, Time = time_slider_start },
|
||||
});
|
||||
|
||||
AddAssert("Tracking kept", assertMaxJudge);
|
||||
}
|
||||
|
||||
private bool assertMaxJudge() => judgementResults?.Any() == true && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
|
||||
|
||||
private void performTest(Beatmap<OsuHitObject> beatmap, List<ReplayFrame> frames)
|
||||
{
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults?.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for completion", () => currentPlayer?.ScoreProcessor.HasCompleted.Value == true);
|
||||
}
|
||||
|
||||
private class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
@ -66,18 +67,25 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
drawableSlider = null;
|
||||
});
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
}
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
[TestCase(0)]
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
[FlakyTest]
|
||||
/*
|
||||
* Fail rate around 0.15%
|
||||
*
|
||||
* TearDown : System.TimeoutException : "wait for seek to finish" timed out
|
||||
* --TearDown
|
||||
* at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
|
||||
* at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
|
||||
* at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
|
||||
*/
|
||||
public void TestSnakingEnabled(int sliderIndex)
|
||||
{
|
||||
AddStep("enable autoplay", () => autoplay = true);
|
||||
base.SetUpSteps();
|
||||
CreateTest();
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
|
||||
retrieveSlider(sliderIndex);
|
||||
@ -98,10 +106,20 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[TestCase(0)]
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
[FlakyTest]
|
||||
/*
|
||||
* Fail rate around 0.15%
|
||||
*
|
||||
* TearDown : System.TimeoutException : "wait for seek to finish" timed out
|
||||
* --TearDown
|
||||
* at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
|
||||
* at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
|
||||
* at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
|
||||
*/
|
||||
public void TestSnakingDisabled(int sliderIndex)
|
||||
{
|
||||
AddStep("have autoplay", () => autoplay = true);
|
||||
base.SetUpSteps();
|
||||
CreateTest();
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
|
||||
retrieveSlider(sliderIndex);
|
||||
@ -121,8 +139,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
AddStep("enable autoplay", () => autoplay = true);
|
||||
setSnaking(true);
|
||||
base.SetUpSteps();
|
||||
|
||||
CreateTest();
|
||||
// repeat might have a chance to update its position depending on where in the frame its hit,
|
||||
// so some leniency is allowed here instead of checking strict equality
|
||||
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
|
||||
@ -133,15 +150,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
AddStep("disable autoplay", () => autoplay = false);
|
||||
setSnaking(true);
|
||||
base.SetUpSteps();
|
||||
|
||||
CreateTest();
|
||||
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
|
||||
}
|
||||
|
||||
private void retrieveSlider(int index)
|
||||
{
|
||||
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
|
||||
addSeekStep(() => slider);
|
||||
addSeekStep(() => slider.StartTime);
|
||||
AddUntilStep("retrieve drawable slider", () =>
|
||||
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
|
||||
}
|
||||
@ -161,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
=> addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
|
||||
|
||||
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
|
||||
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)getSliderStart : getSliderEnd;
|
||||
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? getSliderStart : getSliderEnd;
|
||||
|
||||
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
|
||||
private Vector2 getSliderStart() => getSliderCurve().First();
|
||||
@ -205,16 +221,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
}
|
||||
|
||||
private void addSeekStep(Func<Slider> slider)
|
||||
private void addSeekStep(Func<double> getTime)
|
||||
{
|
||||
AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime));
|
||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||
}
|
||||
|
||||
private void addSeekStep(Func<double> time)
|
||||
{
|
||||
AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
|
||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||
AddStep("seek to time", () => Player.GameplayClockContainer.Seek(getTime()));
|
||||
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(getTime(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
|
||||
}
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };
|
||||
|
@ -1,12 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -26,6 +25,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("speed_difficulty")]
|
||||
public double SpeedDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of clickable objects weighted by difficulty.
|
||||
/// Related to <see cref="SpeedDifficulty"/>
|
||||
/// </summary>
|
||||
[JsonProperty("speed_note_count")]
|
||||
public double SpeedNoteCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the flashlight skill.
|
||||
/// </summary>
|
||||
@ -94,11 +100,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
||||
|
||||
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
|
||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values)
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
{
|
||||
base.FromDatabaseAttributes(values);
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
AimDifficulty = values[ATTRIB_ID_AIM];
|
||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||
@ -108,6 +115,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||
|
||||
DrainRate = onlineInfo.DrainRate;
|
||||
HitCircleCount = onlineInfo.CircleCount;
|
||||
SliderCount = onlineInfo.SliderCount;
|
||||
SpinnerCount = onlineInfo.SpinnerCount;
|
||||
}
|
||||
|
||||
#region Newtonsoft.Json implicit ShouldSerialize() methods
|
||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
|
||||
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
@ -75,6 +76,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Mods = mods,
|
||||
AimDifficulty = aimRating,
|
||||
SpeedDifficulty = speedRating,
|
||||
SpeedNoteCount = speedNotes,
|
||||
FlashlightDifficulty = flashlightRating,
|
||||
SliderFactor = sliderFactor,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
|
@ -163,8 +163,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
}
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
|
||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
|
||||
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
|
||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled.
|
||||
/// </summary>
|
||||
public class Flashlight : OsuStrainSkill
|
||||
public class Flashlight : StrainSkill
|
||||
{
|
||||
private readonly bool hasHiddenMod;
|
||||
|
||||
@ -27,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
private double skillMultiplier => 0.05;
|
||||
private double strainDecayBase => 0.15;
|
||||
protected override double DecayWeight => 1.0;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
public abstract class OsuStrainSkill : StrainSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// The default multiplier applied by <see cref="OsuStrainSkill"/> to the final difficulty value after all other calculations.
|
||||
/// May be overridden via <see cref="DifficultyMultiplier"/>.
|
||||
/// </summary>
|
||||
public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06;
|
||||
|
||||
/// <summary>
|
||||
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
||||
@ -28,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
|
||||
/// </summary>
|
||||
protected virtual double DifficultyMultiplier => 1.06;
|
||||
protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER;
|
||||
|
||||
protected OsuStrainSkill(Mod[] mods)
|
||||
: base(mods)
|
||||
|
@ -8,6 +8,8 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
@ -26,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double DifficultyMultiplier => 1.04;
|
||||
private readonly double greatWindow;
|
||||
|
||||
private readonly List<double> objectStrains = new List<double>();
|
||||
|
||||
public Speed(Mod[] mods, double hitWindowGreat)
|
||||
: base(mods)
|
||||
{
|
||||
@ -43,7 +47,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow);
|
||||
|
||||
return currentStrain * currentRhythm;
|
||||
double totalStrain = currentStrain * currentRhythm;
|
||||
|
||||
objectStrains.Add(totalStrain);
|
||||
|
||||
return totalStrain;
|
||||
}
|
||||
|
||||
public double RelevantNoteCount()
|
||||
{
|
||||
if (objectStrains.Count == 0)
|
||||
return 0;
|
||||
|
||||
double maxStrain = objectStrains.Max();
|
||||
|
||||
if (maxStrain == 0)
|
||||
return 0;
|
||||
|
||||
return objectStrains.Aggregate((total, next) => total + (1.0 / (1.0 + Math.Exp(-(next / maxStrain * 12.0 - 6.0)))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
});
|
||||
|
||||
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
|
||||
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
|
||||
selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid();
|
||||
|
||||
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
|
||||
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
switch (BlueprintContainer.CurrentTool)
|
||||
{
|
||||
case SelectTool _:
|
||||
case SelectTool:
|
||||
if (!EditorBeatmap.SelectedHitObjects.Any())
|
||||
return;
|
||||
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
drawable.ApplyCustomUpdateState += (drawableObject, state) =>
|
||||
drawable.ApplyCustomUpdateState += (drawableObject, _) =>
|
||||
{
|
||||
if (!(drawableObject is DrawableHitCircle drawableHitCircle)) return;
|
||||
|
||||
|
@ -30,9 +30,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
|
||||
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
|
||||
public Bindable<bool> FixedFollowCircleHitArea { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
|
||||
public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true);
|
||||
|
||||
@ -62,10 +59,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case DrawableSlider slider:
|
||||
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
|
||||
break;
|
||||
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
break;
|
||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
switch (drawableObject)
|
||||
{
|
||||
case DrawableSliderTail _:
|
||||
case DrawableSliderTail:
|
||||
using (drawableObject.BeginAbsoluteSequence(fadeStartTime))
|
||||
drawableObject.FadeOut(fadeDuration);
|
||||
|
||||
@ -165,14 +165,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case Slider _:
|
||||
case Slider:
|
||||
return (fadeOutStartTime, longFadeDuration);
|
||||
|
||||
case SliderTick _:
|
||||
case SliderTick:
|
||||
double tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000);
|
||||
return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration);
|
||||
|
||||
case Spinner _:
|
||||
case Spinner:
|
||||
return (fadeOutStartTime + longFadeDuration, fadeOutDuration);
|
||||
|
||||
default:
|
||||
|
@ -44,13 +44,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// apply grow effect
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableSliderHead _:
|
||||
case DrawableSliderTail _:
|
||||
case DrawableSliderHead:
|
||||
case DrawableSliderTail:
|
||||
// special cases we should *not* be scaling.
|
||||
break;
|
||||
|
||||
case DrawableSlider _:
|
||||
case DrawableHitCircle _:
|
||||
case DrawableSlider:
|
||||
case DrawableHitCircle:
|
||||
{
|
||||
using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
|
||||
drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);
|
||||
|
@ -374,7 +374,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
int i = 0;
|
||||
double currentTime = timingPoint.Time;
|
||||
|
||||
while (!definitelyBigger(currentTime, mapEndTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint)
|
||||
while (!definitelyBigger(currentTime, mapEndTime) && ReferenceEquals(controlPointInfo.TimingPointAt(currentTime), timingPoint))
|
||||
{
|
||||
beats.Add(Math.Floor(currentTime));
|
||||
i++;
|
||||
|
@ -34,10 +34,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableSliderHead _:
|
||||
case DrawableSliderTail _:
|
||||
case DrawableSliderTick _:
|
||||
case DrawableSliderRepeat _:
|
||||
case DrawableSliderHead:
|
||||
case DrawableSliderTail:
|
||||
case DrawableSliderTick:
|
||||
case DrawableSliderRepeat:
|
||||
return;
|
||||
|
||||
default:
|
||||
|
@ -29,7 +29,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public DrawableSliderHead HeadCircle => headContainer.Child;
|
||||
public DrawableSliderTail TailCircle => tailContainer.Child;
|
||||
|
||||
public SliderBall Ball { get; private set; }
|
||||
[Cached]
|
||||
public DrawableSliderBall Ball { get; private set; }
|
||||
|
||||
public SkinnableDrawable Body { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
@ -60,6 +62,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public DrawableSlider([CanBeNull] Slider s = null)
|
||||
: base(s)
|
||||
{
|
||||
Ball = new DrawableSliderBall
|
||||
{
|
||||
GetInitialHitAction = () => HeadCircle.HitAction,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -73,13 +82,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
|
||||
Ball = new SliderBall(this)
|
||||
{
|
||||
GetInitialHitAction = () => HeadCircle.HitAction,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
},
|
||||
Ball,
|
||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
||||
};
|
||||
|
||||
|
@ -7,24 +7,21 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
|
||||
public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
|
||||
{
|
||||
public Func<OsuAction?> GetInitialHitAction;
|
||||
|
||||
@ -34,19 +31,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
set => ball.Colour = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether to track accurately to the visual size of this <see cref="SliderBall"/>.
|
||||
/// If <c>false</c>, tracking will be performed at the final scale at all times.
|
||||
/// </summary>
|
||||
public bool InputTracksVisualSize = true;
|
||||
private Drawable followCircle;
|
||||
private Drawable followCircleReceptor;
|
||||
private DrawableSlider drawableSlider;
|
||||
private Drawable ball;
|
||||
|
||||
private readonly Drawable followCircle;
|
||||
private readonly DrawableSlider drawableSlider;
|
||||
private readonly Drawable ball;
|
||||
|
||||
public SliderBall(DrawableSlider drawableSlider)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableSlider)
|
||||
{
|
||||
this.drawableSlider = drawableSlider;
|
||||
this.drawableSlider = (DrawableSlider)drawableSlider;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
@ -54,13 +47,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
followCircle = new FollowCircleContainer
|
||||
followCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
|
||||
},
|
||||
followCircleReceptor = new CircularContainer
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true
|
||||
},
|
||||
ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
|
||||
{
|
||||
@ -104,14 +103,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
tracking = value;
|
||||
|
||||
if (InputTracksVisualSize)
|
||||
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
|
||||
else
|
||||
{
|
||||
// We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration.
|
||||
followCircle.ScaleTo(tracking ? 2.4f : 1f);
|
||||
}
|
||||
followCircleReceptor.Scale = new Vector2(tracking ? 2.4f : 1f);
|
||||
|
||||
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
|
||||
followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
@ -170,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
// in valid time range
|
||||
Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime &&
|
||||
// in valid position range
|
||||
lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
|
||||
lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) &&
|
||||
// valid action
|
||||
(actions?.Any(isValidTrackingAction) ?? false);
|
||||
|
||||
@ -208,74 +202,5 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
|
||||
lastPosition = Position;
|
||||
}
|
||||
|
||||
private class FollowCircleContainer : CircularContainer
|
||||
{
|
||||
public override bool HandlePositionalInput => true;
|
||||
}
|
||||
|
||||
public class DefaultFollowCircle : CompositeDrawable
|
||||
{
|
||||
public DefaultFollowCircle()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 5,
|
||||
BorderColour = Color4.Orange,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.Orange,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.2f,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultSliderBall : CompositeDrawable
|
||||
{
|
||||
private Box box;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableObject, ISkinSource skin)
|
||||
{
|
||||
var slider = (DrawableSlider)drawableObject;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
BorderThickness = 10,
|
||||
BorderColour = Color4.White,
|
||||
Alpha = 1,
|
||||
Child = box = new Box
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
}
|
||||
};
|
||||
|
||||
slider.Tracking.BindValueChanged(trackingChanged, true);
|
||||
}
|
||||
|
||||
private void trackingChanged(ValueChangedEvent<bool> tracking) =>
|
||||
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
|
||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||
}
|
||||
|
||||
|
@ -120,19 +120,19 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case OsuModAutopilot _:
|
||||
case OsuModAutopilot:
|
||||
value |= LegacyMods.Autopilot;
|
||||
break;
|
||||
|
||||
case OsuModSpunOut _:
|
||||
case OsuModSpunOut:
|
||||
value |= LegacyMods.SpunOut;
|
||||
break;
|
||||
|
||||
case OsuModTarget _:
|
||||
case OsuModTarget:
|
||||
value |= LegacyMods.Target;
|
||||
break;
|
||||
|
||||
case OsuModTouchDevice _:
|
||||
case OsuModTouchDevice:
|
||||
value |= LegacyMods.TouchDevice;
|
||||
break;
|
||||
}
|
||||
|
@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
protected override string RulesetPrefix => OsuRuleset.SHORT_NAME;
|
||||
|
||||
protected override string ComponentName => Component.ToString().ToLower();
|
||||
protected override string ComponentName => Component.ToString().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
@ -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 osuTK;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -95,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
{
|
||||
double endTime = prev.GetEndTime();
|
||||
|
||||
HitWindows hitWindows = null;
|
||||
HitWindows? hitWindows = null;
|
||||
|
||||
switch (h)
|
||||
{
|
||||
@ -107,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
hitWindows = slider.TailCircle.HitWindows;
|
||||
break;
|
||||
|
||||
case Spinner _:
|
||||
case Spinner:
|
||||
hitWindows = defaultHitWindows;
|
||||
break;
|
||||
}
|
||||
@ -245,7 +243,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
}
|
||||
|
||||
double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
|
||||
OsuReplayFrame lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
|
||||
OsuReplayFrame? lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
|
||||
|
||||
if (timeDifference > 0)
|
||||
{
|
||||
|
@ -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 osuTK;
|
||||
using osu.Game.Beatmaps;
|
||||
using System;
|
||||
|
@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
|
@ -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.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
@ -28,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
Actions.AddRange(actions);
|
||||
}
|
||||
|
||||
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
|
||||
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
|
||||
{
|
||||
Position = currentFrame.Position;
|
||||
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
|
||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case HitCircle _:
|
||||
case HitCircle:
|
||||
return new OsuHitCircleJudgementResult(hitObject, judgement);
|
||||
|
||||
default:
|
||||
|
@ -0,0 +1,33 @@
|
||||
// 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;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
public class DefaultFollowCircle : CompositeDrawable
|
||||
{
|
||||
public DefaultFollowCircle()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 5,
|
||||
BorderColour = Color4.Orange,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.Orange,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.2f,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
60
osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
Normal file
60
osu.Game.Rulesets.Osu/Skinning/Default/DefaultSliderBall.cs
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
public class DefaultSliderBall : CompositeDrawable
|
||||
{
|
||||
private Box box;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableObject, ISkinSource skin)
|
||||
{
|
||||
var slider = (DrawableSlider)drawableObject;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(radius / OsuHitObject.OBJECT_RADIUS),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Blending = BlendingParameters.Additive,
|
||||
BorderThickness = 10,
|
||||
BorderColour = Color4.White,
|
||||
Alpha = 1,
|
||||
Child = box = new Box
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.White,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
}
|
||||
};
|
||||
|
||||
slider.Tracking.BindValueChanged(trackingChanged, true);
|
||||
}
|
||||
|
||||
private void trackingChanged(ValueChangedEvent<bool> tracking) =>
|
||||
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
|
||||
}
|
||||
}
|
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
|
||||
complete.BindValueChanged(complete => updateDiscColour(complete.NewValue, 200));
|
||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||
|
||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||
@ -137,6 +137,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
this.ScaleTo(initial_scale);
|
||||
this.RotateTo(0);
|
||||
|
||||
updateDiscColour(false);
|
||||
|
||||
using (BeginDelayedSequence(spinner.TimePreempt / 2))
|
||||
{
|
||||
// constant ambient rotation to give the spinner "spinning" character.
|
||||
@ -177,12 +179,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
}
|
||||
}
|
||||
|
||||
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
|
||||
updateComplete(state == ArmedState.Hit, 0);
|
||||
if (drawableSpinner.Result?.TimeCompleted is double completionTime)
|
||||
{
|
||||
using (BeginAbsoluteSequence(completionTime))
|
||||
updateDiscColour(true, 200);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateComplete(bool complete, double duration)
|
||||
private void updateDiscColour(bool complete, double duration = 0)
|
||||
{
|
||||
var colour = complete ? completeColour : normalColour;
|
||||
|
||||
|
22
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
Normal file
22
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// 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;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public class LegacyFollowCircle : CompositeDrawable
|
||||
{
|
||||
public LegacyFollowCircle(Drawable animationContent)
|
||||
{
|
||||
// follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
|
||||
animationContent.Scale *= 0.5f;
|
||||
animationContent.Anchor = Anchor.Centre;
|
||||
animationContent.Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
InternalChild = animationContent;
|
||||
}
|
||||
}
|
||||
}
|
@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
break;
|
||||
|
||||
case DrawableSpinnerBonusTick _:
|
||||
case DrawableSpinnerBonusTick:
|
||||
if (state == ArmedState.Hit)
|
||||
glow.FlashColour(Color4.White, 200);
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -18,8 +18,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
private readonly ISkin skin;
|
||||
|
||||
private Sprite layerNd;
|
||||
private Sprite layerSpec;
|
||||
[Resolved(canBeNull: true)]
|
||||
private DrawableHitObject? parentObject { get; set; }
|
||||
|
||||
private Sprite layerNd = null!;
|
||||
private Sprite layerSpec = null!;
|
||||
|
||||
public LegacySliderBall(Drawable animationContent, ISkin skin)
|
||||
{
|
||||
@ -58,6 +61,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (parentObject != null)
|
||||
{
|
||||
parentObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
updateStateTransforms(parentObject, parentObject.State.Value);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
@ -68,5 +82,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
layerNd.Rotation = -appliedRotation;
|
||||
layerSpec.Rotation = -appliedRotation;
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
|
||||
{
|
||||
// Gets called by slider ticks, tails, etc., leading to duplicated
|
||||
// animations which in this case have no visual impact (due to
|
||||
// instant fade) but may negatively affect performance
|
||||
if (drawableObject is not DrawableSlider)
|
||||
return;
|
||||
|
||||
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
|
||||
this.FadeIn();
|
||||
|
||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
||||
this.FadeOut();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (parentObject != null)
|
||||
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,11 +41,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
return this.GetAnimation(component.LookupName, false, false);
|
||||
|
||||
case OsuSkinComponents.SliderFollowCircle:
|
||||
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
|
||||
if (followCircle != null)
|
||||
// follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x
|
||||
followCircle.Scale *= 0.5f;
|
||||
return followCircle;
|
||||
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true);
|
||||
if (followCircleContent != null)
|
||||
return new LegacyFollowCircle(followCircleContent);
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.SliderBall:
|
||||
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
switch (obj)
|
||||
{
|
||||
case DrawableSpinner _:
|
||||
case DrawableSpinner:
|
||||
continue;
|
||||
|
||||
case DrawableSlider slider:
|
||||
|
@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
// note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded.
|
||||
switch (drawable)
|
||||
{
|
||||
case DrawableSpinner _:
|
||||
case DrawableSpinner:
|
||||
spinnerProxies.Add(drawable.CreateProxy());
|
||||
break;
|
||||
|
||||
|
@ -88,11 +88,11 @@ namespace osu.Game.Rulesets.Osu.Utils
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case HitCircle _:
|
||||
case HitCircle:
|
||||
shift = clampHitCircleToPlayfield(current);
|
||||
break;
|
||||
|
||||
case Slider _:
|
||||
case Slider:
|
||||
shift = clampSliderToPlayfield(current);
|
||||
break;
|
||||
}
|
||||
|
@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
createDrawableRuleset();
|
||||
|
||||
assertStateAfterResult(new JudgementResult(new Swell(), new TaikoSwellJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Clear);
|
||||
AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLower()}", () => allMascotsIn(expectedStateAfterClear));
|
||||
AddUntilStep($"state reverts to {expectedStateAfterClear.ToString().ToLowerInvariant()}", () => allMascotsIn(expectedStateAfterClear));
|
||||
}
|
||||
|
||||
private void setBeatmap(bool kiai = false)
|
||||
@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
{
|
||||
TaikoMascotAnimationState[] mascotStates = null;
|
||||
|
||||
AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
|
||||
AddStep($"{judgementResult.Type.ToString().ToLowerInvariant()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
|
||||
() =>
|
||||
{
|
||||
applyNewResult(judgementResult);
|
||||
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
|
||||
});
|
||||
|
||||
AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState));
|
||||
AddAssert($"state is {expectedState.ToString().ToLowerInvariant()}", () => mascotStates.All(state => state == expectedState));
|
||||
}
|
||||
|
||||
private void applyNewResult(JudgementResult judgementResult)
|
||||
|
@ -24,22 +24,25 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
new object[] { LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) } },
|
||||
new object[] { LegacyMods.Relax, new[] { typeof(TaikoModRelax) } },
|
||||
new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } },
|
||||
new object[] { LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) } },
|
||||
new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } },
|
||||
new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } },
|
||||
new object[] { LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) } },
|
||||
new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } },
|
||||
new object[] { LegacyMods.Cinema, new[] { typeof(TaikoModCinema) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } }
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(taiko_mod_mapping))]
|
||||
[TestCase(LegacyMods.Cinema, new[] { typeof(TaikoModCinema) })]
|
||||
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })]
|
||||
[TestCase(LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) })]
|
||||
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })]
|
||||
[TestCase(LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) })]
|
||||
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })]
|
||||
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
|
||||
|
||||
[TestCaseSource(nameof(taiko_mod_mapping))]
|
||||
[TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })]
|
||||
[TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })]
|
||||
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })]
|
||||
public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods);
|
||||
|
||||
[TestCaseSource(nameof(taiko_mod_mapping))]
|
||||
public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods);
|
||||
|
||||
protected override Ruleset CreateRuleset() => new TaikoRuleset();
|
||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
AddStep("Setup judgements", () =>
|
||||
{
|
||||
judged = false;
|
||||
Player.ScoreProcessor.NewJudgement += b => judged = true;
|
||||
Player.ScoreProcessor.NewJudgement += _ => judged = true;
|
||||
});
|
||||
AddUntilStep("swell judged", () => judged);
|
||||
AddAssert("failed", () => Player.GameplayState.HasFailed);
|
||||
|
@ -1,10 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
@ -57,9 +56,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values)
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
{
|
||||
base.FromDatabaseAttributes(values);
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
|
@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case DrawableDrumRollTick _:
|
||||
case DrawableHit _:
|
||||
case DrawableDrumRollTick:
|
||||
case DrawableHit:
|
||||
double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier;
|
||||
double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time;
|
||||
double duration = preempt * fade_out_duration;
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
DisplayColour.Value = Type == HitType.Centre ? COLOUR_CENTRE : COLOUR_RIM;
|
||||
});
|
||||
|
||||
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples());
|
||||
SamplesBindable.BindCollectionChanged((_, _) => updateTypeFromSamples());
|
||||
}
|
||||
|
||||
private void updateTypeFromSamples()
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
protected TaikoStrongableHitObject()
|
||||
{
|
||||
IsStrongBindable.BindValueChanged(_ => updateSamplesFromType());
|
||||
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples());
|
||||
SamplesBindable.BindCollectionChanged((_, _) => updateTypeFromSamples());
|
||||
}
|
||||
|
||||
private void updateTypeFromSamples()
|
||||
|
@ -1,10 +1,9 @@
|
||||
// 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 osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
@ -119,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
|
||||
var nextHitObject = GetNextObject(i); // Get the next object that requires pressing the same button
|
||||
|
||||
bool canDelayKeyUp = nextHitObject == null || nextHitObject.StartTime > endTime + KEY_UP_DELAY;
|
||||
double calculatedDelay = canDelayKeyUp ? KEY_UP_DELAY : (nextHitObject.StartTime - endTime) * 0.9;
|
||||
double calculatedDelay = canDelayKeyUp ? KEY_UP_DELAY : (nextHitObject.AsNonNull().StartTime - endTime) * 0.9;
|
||||
Frames.Add(new TaikoReplayFrame(endTime + calculatedDelay));
|
||||
|
||||
hitButton = !hitButton;
|
||||
|
@ -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 osu.Game.Rulesets.Replays;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
@ -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.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
@ -25,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
|
||||
Actions.AddRange(actions);
|
||||
}
|
||||
|
||||
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
|
||||
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
|
||||
{
|
||||
if (currentFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim);
|
||||
if (currentFrame.MouseRight2) Actions.Add(TaikoAction.RightRim);
|
||||
|
@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
protected override string RulesetPrefix => TaikoRuleset.SHORT_NAME;
|
||||
|
||||
protected override string ComponentName => Component.ToString().ToLower();
|
||||
protected override string ComponentName => Component.ToString().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
@ -139,10 +139,10 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
|
||||
{
|
||||
var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
|
||||
var texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}{frameIndex}");
|
||||
|
||||
if (frameIndex == 0 && texture == null)
|
||||
texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}");
|
||||
texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}");
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
barLinePlayfield.Add(barLine);
|
||||
break;
|
||||
|
||||
case DrawableTaikoHitObject _:
|
||||
case DrawableTaikoHitObject:
|
||||
base.Add(h);
|
||||
break;
|
||||
|
||||
@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
case DrawableBarLine barLine:
|
||||
return barLinePlayfield.Remove(barLine);
|
||||
|
||||
case DrawableTaikoHitObject _:
|
||||
case DrawableTaikoHitObject:
|
||||
return base.Remove(h);
|
||||
|
||||
default:
|
||||
@ -280,12 +280,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
switch (result.Judgement)
|
||||
{
|
||||
case TaikoStrongJudgement _:
|
||||
case TaikoStrongJudgement:
|
||||
if (result.IsHit)
|
||||
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(result);
|
||||
break;
|
||||
|
||||
case TaikoDrumRollTickJudgement _:
|
||||
case TaikoDrumRollTickJudgement:
|
||||
if (!result.IsHit)
|
||||
break;
|
||||
|
||||
|
@ -157,7 +157,7 @@ namespace osu.Game.Tests.Beatmaps
|
||||
[TestCase(8.3, DifficultyRating.ExpertPlus)]
|
||||
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
|
||||
{
|
||||
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
|
||||
var actualBracket = StarDifficulty.GetDifficultyRating(starRating);
|
||||
|
||||
Assert.AreEqual(expectedBracket, actualBracket);
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
public async Task TestImportMalformedDatabase()
|
||||
{
|
||||
bool exceptionThrown = false;
|
||||
UnhandledExceptionEventHandler setException = (_, __) => exceptionThrown = true;
|
||||
UnhandledExceptionEventHandler setException = (_, _) => exceptionThrown = true;
|
||||
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
{
|
||||
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
{
|
||||
string firstRunName;
|
||||
|
||||
using (var host = new CleanRunHeadlessGameHost(bypassCleanup: true))
|
||||
using (var host = new CleanRunHeadlessGameHost(bypassCleanupOnDispose: true))
|
||||
{
|
||||
firstRunName = host.Name;
|
||||
|
||||
|
@ -15,7 +15,6 @@ using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
@ -36,13 +35,11 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using (var importer = new BeatmapImporter(storage, realm))
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Live<BeatmapSetInfo>? beatmapSet;
|
||||
|
||||
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
||||
beatmapSet = await importer.Import(reader);
|
||||
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
|
||||
|
||||
Assert.NotNull(beatmapSet);
|
||||
Debug.Assert(beatmapSet != null);
|
||||
@ -80,13 +77,11 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using (var importer = new BeatmapImporter(storage, realm))
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Live<BeatmapSetInfo>? beatmapSet;
|
||||
|
||||
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
||||
beatmapSet = await importer.Import(reader);
|
||||
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
|
||||
|
||||
Assert.NotNull(beatmapSet);
|
||||
Debug.Assert(beatmapSet != null);
|
||||
@ -141,16 +136,14 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
var manager = new ModelManager<BeatmapSetInfo>(storage, realm);
|
||||
|
||||
using (var importer = new BeatmapImporter(storage, realm))
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
Live<BeatmapSetInfo>? beatmapSet;
|
||||
|
||||
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
beatmapSet = await importer.Import(reader);
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
var beatmapSet = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
|
||||
|
||||
Assert.NotNull(beatmapSet);
|
||||
Debug.Assert(beatmapSet != null);
|
||||
@ -170,16 +163,12 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using (var importer = new BeatmapImporter(storage, realm))
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
Live<BeatmapSetInfo>? imported;
|
||||
|
||||
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
||||
{
|
||||
imported = await importer.Import(reader);
|
||||
EnsureLoaded(realm.Realm);
|
||||
}
|
||||
var imported = await importer.Import(new ImportTask(TestResources.GetTestBeatmapStream(), "renatus.osz"));
|
||||
EnsureLoaded(realm.Realm);
|
||||
|
||||
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
|
||||
|
||||
@ -202,7 +191,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -214,7 +203,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -232,7 +221,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -246,7 +235,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? tempPath = TestResources.GetTestBeatmapForImport();
|
||||
@ -276,7 +265,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -291,12 +280,48 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportDirectoryWithEmptyOsuFiles()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
foreach (var file in new DirectoryInfo(extractedFolder).GetFiles("*.osu"))
|
||||
{
|
||||
using (file.Open(FileMode.Create))
|
||||
{
|
||||
// empty file.
|
||||
}
|
||||
}
|
||||
|
||||
var imported = await importer.Import(new ImportTask(extractedFolder));
|
||||
Assert.IsNull(imported);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenImportWithReZip()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -345,7 +370,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -396,7 +421,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -444,7 +469,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -492,7 +517,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -527,7 +552,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var progressNotification = new ImportProgressNotification();
|
||||
@ -565,7 +590,7 @@ namespace osu.Game.Tests.Database
|
||||
Interlocked.Increment(ref loggedExceptionCount);
|
||||
};
|
||||
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -586,6 +611,12 @@ namespace osu.Game.Tests.Database
|
||||
using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
|
||||
using (var zip = ZipArchive.Open(brokenOsz))
|
||||
{
|
||||
foreach (var entry in zip.Entries.ToArray())
|
||||
{
|
||||
if (entry.Key.EndsWith(".osu", StringComparison.InvariantCulture))
|
||||
zip.RemoveEntry(entry);
|
||||
}
|
||||
|
||||
zip.AddEntry("broken.osu", brokenOsu, false);
|
||||
zip.SaveTo(outStream, CompressionType.Deflate);
|
||||
}
|
||||
@ -606,7 +637,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
checkSingleReferencedFileCount(realm.Realm, 18);
|
||||
|
||||
Assert.AreEqual(1, loggedExceptionCount);
|
||||
Assert.AreEqual(0, loggedExceptionCount);
|
||||
|
||||
File.Delete(brokenTempFilename);
|
||||
});
|
||||
@ -617,7 +648,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm, batchImport: true);
|
||||
@ -644,7 +675,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realmFactory);
|
||||
var importer = new BeatmapImporter(storage, realmFactory);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Realm);
|
||||
@ -676,7 +707,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -703,7 +734,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realm.Realm);
|
||||
@ -729,7 +760,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealm((realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
var metadata = new BeatmapMetadata
|
||||
@ -760,7 +791,7 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
};
|
||||
|
||||
var imported = importer.Import(toImport);
|
||||
var imported = importer.ImportModel(toImport);
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
@ -777,7 +808,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -794,7 +825,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -830,7 +861,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -872,7 +903,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
@ -923,7 +954,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(storage, realm);
|
||||
var importer = new BeatmapImporter(storage, realm);
|
||||
using var store = new RealmRulesetStore(realm, storage);
|
||||
|
||||
string? temp = TestResources.GetTestBeatmapForImport();
|
||||
|
@ -2,11 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
@ -27,12 +30,91 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAsyncWriteAsync()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realm, _) =>
|
||||
{
|
||||
await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
Assert.That(realm.Run(r => r.All<BeatmapSetInfo>().Count()), Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAsyncWriteWhileBlocking()
|
||||
{
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
Task writeTask;
|
||||
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
{
|
||||
writeTask = realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||
Thread.Sleep(100);
|
||||
Assert.That(writeTask.IsCompleted, Is.False);
|
||||
}
|
||||
|
||||
writeTask.WaitSafely();
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
Assert.That(realm.Run(r => r.All<BeatmapSetInfo>().Count()), Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAsyncWrite()
|
||||
{
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
Assert.That(realm.Run(r => r.All<BeatmapSetInfo>().Count()), Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAsyncWriteAfterDisposal()
|
||||
{
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
realm.Dispose();
|
||||
Assert.ThrowsAsync<ObjectDisposedException>(() => realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAsyncWriteBeforeDisposal()
|
||||
{
|
||||
ManualResetEventSlim resetEvent = new ManualResetEventSlim();
|
||||
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
var writeTask = realm.WriteAsync(r =>
|
||||
{
|
||||
// ensure that disposal blocks for our execution
|
||||
Assert.That(resetEvent.Wait(100), Is.False);
|
||||
|
||||
r.Add(TestResources.CreateTestBeatmapSetInfo());
|
||||
});
|
||||
|
||||
realm.Dispose();
|
||||
resetEvent.Set();
|
||||
|
||||
writeTask.WaitSafely();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks
|
||||
/// due to context fetching semaphores.
|
||||
@ -46,7 +128,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
realm.RegisterCustomSubscription(r =>
|
||||
{
|
||||
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((sender, changes, error) =>
|
||||
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((_, _, _) =>
|
||||
{
|
||||
realm.Run(_ =>
|
||||
{
|
||||
@ -79,15 +161,15 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
hasThreadedUsage.Set();
|
||||
|
||||
stopThreadedUsage.Wait();
|
||||
stopThreadedUsage.Wait(60000);
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler);
|
||||
|
||||
hasThreadedUsage.Wait();
|
||||
hasThreadedUsage.Wait(60000);
|
||||
|
||||
Assert.Throws<TimeoutException>(() =>
|
||||
{
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
{
|
||||
}
|
||||
});
|
||||
@ -95,7 +177,7 @@ namespace osu.Game.Tests.Database
|
||||
stopThreadedUsage.Set();
|
||||
|
||||
// Ensure we can block a second time after the usage has ended.
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
{
|
||||
}
|
||||
});
|
||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
migratedStorage.DeleteDirectory(string.Empty);
|
||||
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
{
|
||||
storage.Migrate(migratedStorage);
|
||||
}
|
||||
@ -59,6 +59,64 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFailedWritePerformsRollback()
|
||||
{
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
realm.Write(r =>
|
||||
{
|
||||
r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()));
|
||||
throw new InvalidOperationException();
|
||||
});
|
||||
});
|
||||
|
||||
Assert.That(realm.Run(r => r.All<BeatmapInfo>()), Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFailedNestedWritePerformsRollback()
|
||||
{
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
realm.Write(r =>
|
||||
{
|
||||
realm.Write(_ =>
|
||||
{
|
||||
r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()));
|
||||
throw new InvalidOperationException();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Assert.That(realm.Run(r => r.All<BeatmapInfo>()), Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNestedWriteCalls()
|
||||
{
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
|
||||
|
||||
var liveBeatmap = beatmap.ToLive(realm);
|
||||
|
||||
realm.Run(r =>
|
||||
r.Write(_ =>
|
||||
r.Write(_ =>
|
||||
r.Add(beatmap)))
|
||||
);
|
||||
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAccessAfterAttach()
|
||||
{
|
||||
@ -91,6 +149,25 @@ namespace osu.Game.Tests.Database
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTransactionRolledBackOnException()
|
||||
{
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
|
||||
|
||||
realm.Run(r => r.Write(_ => r.Add(beatmap)));
|
||||
|
||||
var liveBeatmap = beatmap.ToLive(realm);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => liveBeatmap.PerformWrite(l => throw new InvalidOperationException()));
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
|
||||
liveBeatmap.PerformWrite(l => l.Hidden = true);
|
||||
Assert.IsTrue(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScopedReadWithoutContext()
|
||||
{
|
||||
@ -189,7 +266,7 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
|
||||
// Can't be used, even from within a valid context.
|
||||
realm.Run(threadContext =>
|
||||
realm.Run(_ =>
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
@ -84,11 +83,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
// Without forcing the write onto its own thread, realm will internally run the operation synchronously, which can cause a deadlock with `WaitSafely`.
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||
}).WaitSafely();
|
||||
realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
@ -141,7 +136,7 @@ namespace osu.Game.Tests.Database
|
||||
resolvedItems = null;
|
||||
lastChanges = null;
|
||||
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
Assert.That(resolvedItems, Is.Empty);
|
||||
|
||||
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||
@ -159,7 +154,7 @@ namespace osu.Game.Tests.Database
|
||||
testEventsArriving(false);
|
||||
|
||||
// And make sure even after another context loss we don't get firings.
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
Assert.That(resolvedItems, Is.Null);
|
||||
|
||||
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||
@ -217,7 +212,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Assert.That(beatmapSetInfo, Is.Not.Null);
|
||||
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
{
|
||||
// custom disposal action fired when context lost.
|
||||
Assert.That(beatmapSetInfo, Is.Null);
|
||||
@ -231,7 +226,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Assert.That(beatmapSetInfo, Is.Null);
|
||||
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
Assert.That(beatmapSetInfo, Is.Null);
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
@ -256,7 +251,7 @@ namespace osu.Game.Tests.Database
|
||||
Assert.That(receivedValue, Is.Not.Null);
|
||||
receivedValue = null;
|
||||
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
{
|
||||
}
|
||||
|
||||
@ -267,7 +262,7 @@ namespace osu.Game.Tests.Database
|
||||
subscription.Dispose();
|
||||
receivedValue = null;
|
||||
|
||||
using (realm.BlockAllOperations())
|
||||
using (realm.BlockAllOperations("testing"))
|
||||
Assert.That(receivedValue, Is.Null);
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
@ -192,7 +192,7 @@ namespace osu.Game.Tests.Gameplay
|
||||
AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
|
||||
AddAssert("not failed", () => !processor.HasFailed);
|
||||
|
||||
AddStep($"apply {resultApplied.ToString().ToLower()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
|
||||
AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
|
||||
AddAssert("failed", () => processor.HasFailed);
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -78,6 +77,16 @@ namespace osu.Game.Tests.Gameplay
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
/*
|
||||
* Fail rate around 0.15%
|
||||
*
|
||||
* TearDown : osu.Framework.Testing.Drawables.Steps.AssertButton+TracedException : gameplay clock time = 2500
|
||||
* --TearDown
|
||||
* at osu.Framework.Threading.ScheduledDelegate.RunTaskInternal()
|
||||
* at osu.Framework.Threading.Scheduler.Update()
|
||||
* at osu.Framework.Graphics.Drawable.UpdateSubTree()
|
||||
*/
|
||||
public void TestSeekPerformsInGameplayTime(
|
||||
[Values(1.0, 0.5, 2.0)] double clockRate,
|
||||
[Values(0.0, 200.0, -200.0)] double userOffset,
|
||||
@ -106,10 +115,10 @@ namespace osu.Game.Tests.Gameplay
|
||||
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
|
||||
|
||||
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
|
||||
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
|
||||
AddStep("gameplay clock time = 2500", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 2500, 10f));
|
||||
|
||||
AddStep("seek to 10000", () => gameplayClockContainer.Seek(10000));
|
||||
AddAssert("gameplay clock time = 10000", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 10000, 10f));
|
||||
AddStep("gameplay clock time = 10000", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 10000, 10f));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -44,8 +44,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestCustomDirectory()
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (prepareCustomPath(out string customPath))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
|
||||
@ -63,7 +62,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
cleanupPath(customPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -71,8 +69,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestSubDirectoryLookup()
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (prepareCustomPath(out string customPath))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
using (var storageConfig = new StorageConfigManager(host.InitialStorage))
|
||||
@ -97,7 +94,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
cleanupPath(customPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -105,8 +101,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestMigration()
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (prepareCustomPath(out string customPath))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
@ -173,7 +168,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
cleanupPath(customPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,9 +175,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestMigrationBetweenTwoTargets()
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
string customPath2 = prepareCustomPath();
|
||||
|
||||
using (prepareCustomPath(out string customPath))
|
||||
using (prepareCustomPath(out string customPath2))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
@ -205,8 +198,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
cleanupPath(customPath);
|
||||
cleanupPath(customPath2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -214,8 +205,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestMigrationToSameTargetFails()
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (prepareCustomPath(out string customPath))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
@ -228,7 +218,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
cleanupPath(customPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -236,9 +225,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestMigrationFailsOnExistingData()
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
string customPath2 = prepareCustomPath();
|
||||
|
||||
using (prepareCustomPath(out string customPath))
|
||||
using (prepareCustomPath(out string customPath2))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
@ -254,7 +242,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
Assert.That(File.Exists(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME)));
|
||||
|
||||
Directory.CreateDirectory(customPath2);
|
||||
File.Copy(Path.Combine(customPath, OsuGameBase.CLIENT_DATABASE_FILENAME), Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME));
|
||||
File.WriteAllText(Path.Combine(customPath2, OsuGameBase.CLIENT_DATABASE_FILENAME), "I am a text");
|
||||
|
||||
// Fails because file already exists.
|
||||
Assert.Throws<ArgumentException>(() => osu.Migrate(customPath2));
|
||||
@ -267,8 +255,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
cleanupPath(customPath);
|
||||
cleanupPath(customPath2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -276,8 +262,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestMigrationToNestedTargetFails()
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (prepareCustomPath(out string customPath))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
@ -298,7 +283,6 @@ namespace osu.Game.Tests.NonVisual
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
cleanupPath(customPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -306,8 +290,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestMigrationToSeeminglyNestedTarget()
|
||||
{
|
||||
string customPath = prepareCustomPath();
|
||||
|
||||
using (prepareCustomPath(out string customPath))
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
@ -328,7 +311,26 @@ namespace osu.Game.Tests.NonVisual
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
cleanupPath(customPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackupCreatedOnCorruptRealm()
|
||||
{
|
||||
using (var host = new CustomTestHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(host.InitialStorage.GetFullPath(OsuGameBase.CLIENT_DATABASE_FILENAME, true), "i am definitely not a realm file");
|
||||
|
||||
LoadOsuIntoHost(host);
|
||||
|
||||
Assert.That(host.InitialStorage.GetFiles(string.Empty, "*_corrupt.realm"), Has.One.Items);
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -343,14 +345,17 @@ namespace osu.Game.Tests.NonVisual
|
||||
return path;
|
||||
}
|
||||
|
||||
private static string prepareCustomPath() => Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}");
|
||||
private static IDisposable prepareCustomPath(out string path)
|
||||
{
|
||||
path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}");
|
||||
return new InvokeOnDisposal<string>(path, cleanupPath);
|
||||
}
|
||||
|
||||
private static void cleanupPath(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
Directory.Delete(path, true);
|
||||
if (Directory.Exists(path)) Directory.Delete(path, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -362,7 +367,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
public Storage InitialStorage { get; }
|
||||
|
||||
public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
|
||||
: base(callingMethodName: callingMethodName)
|
||||
: base(callingMethodName: callingMethodName, bypassCleanupOnSetup: true)
|
||||
{
|
||||
string defaultStorageLocation = getDefaultLocationFor(this);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user