mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 10:33:30 +08:00
Merge branch 'master' into pp_refactoring_osustrainskill
This commit is contained in:
commit
d54c6aefbe
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@ -121,9 +121,7 @@ jobs:
|
|||||||
|
|
||||||
build-only-ios:
|
build-only-ios:
|
||||||
name: Build only (iOS)
|
name: Build only (iOS)
|
||||||
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3.
|
runs-on: macos-latest
|
||||||
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images)
|
|
||||||
runs-on: macos-13
|
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -137,8 +135,5 @@ jobs:
|
|||||||
- name: Install .NET Workloads
|
- name: Install .NET Workloads
|
||||||
run: dotnet workload install maui-ios
|
run: dotnet workload install maui-ios
|
||||||
|
|
||||||
- name: Select Xcode 15.2
|
|
||||||
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build -c Debug osu.iOS
|
run: dotnet build -c Debug osu.iOS
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.802.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.912.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -80,7 +80,7 @@ namespace osu.Android
|
|||||||
host.Window.CursorState |= CursorState.Hidden;
|
host.Window.CursorState |= CursorState.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
|
||||||
|
|
||||||
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
|
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.Versioning;
|
using System.Runtime.Versioning;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using osu.Desktop.Performance;
|
using osu.Desktop.Performance;
|
||||||
using osu.Desktop.Security;
|
using osu.Desktop.Security;
|
||||||
@ -95,42 +95,20 @@ namespace osu.Desktop
|
|||||||
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsPackageManaged => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"));
|
||||||
|
|
||||||
protected override UpdateManager CreateUpdateManager()
|
protected override UpdateManager CreateUpdateManager()
|
||||||
{
|
{
|
||||||
string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER");
|
if (IsPackageManaged)
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(packageManaged))
|
|
||||||
return new NoActionUpdateManager();
|
return new NoActionUpdateManager();
|
||||||
|
|
||||||
switch (RuntimeInfo.OS)
|
return new VelopackUpdateManager();
|
||||||
{
|
|
||||||
case RuntimeInfo.Platform.Windows:
|
|
||||||
Debug.Assert(OperatingSystem.IsWindows());
|
|
||||||
|
|
||||||
return new SquirrelUpdateManager();
|
|
||||||
|
|
||||||
default:
|
|
||||||
return new SimpleUpdateManager();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool RestartAppWhenExited()
|
public override bool RestartAppWhenExited()
|
||||||
{
|
{
|
||||||
switch (RuntimeInfo.OS)
|
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
|
||||||
{
|
return true;
|
||||||
case RuntimeInfo.Platform.Windows:
|
|
||||||
Debug.Assert(OperatingSystem.IsWindows());
|
|
||||||
|
|
||||||
// Of note, this is an async method in squirrel that adds an arbitrary delay before returning
|
|
||||||
// likely to ensure the external process is in a good state.
|
|
||||||
//
|
|
||||||
// We're not waiting on that here, but the outro playing before the actual exit should be enough
|
|
||||||
// to cover this.
|
|
||||||
Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.RestartAppWhenExited();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -14,7 +14,7 @@ using osu.Game;
|
|||||||
using osu.Game.IPC;
|
using osu.Game.IPC;
|
||||||
using osu.Game.Tournament;
|
using osu.Game.Tournament;
|
||||||
using SDL;
|
using SDL;
|
||||||
using Squirrel;
|
using Velopack;
|
||||||
|
|
||||||
namespace osu.Desktop
|
namespace osu.Desktop
|
||||||
{
|
{
|
||||||
@ -31,19 +31,11 @@ namespace osu.Desktop
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
/*
|
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
|
||||||
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
|
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
|
||||||
*
|
// last time has been fixed, let's not tempt fate.
|
||||||
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
|
setupVelopack();
|
||||||
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
|
|
||||||
* namely by checking loaded assemblies:
|
|
||||||
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
|
|
||||||
*
|
|
||||||
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
|
|
||||||
* the app will then do completely broken things like:
|
|
||||||
* - not creating system shortcuts (as the logic is if'd out if "running tests")
|
|
||||||
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
|
|
||||||
*/
|
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
var windowsVersion = Environment.OSVersion.Version;
|
var windowsVersion = Environment.OSVersion.Version;
|
||||||
@ -66,8 +58,6 @@ namespace osu.Desktop
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSquirrel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NVIDIA profiles are based on the executable name of a process.
|
// NVIDIA profiles are based on the executable name of a process.
|
||||||
@ -177,32 +167,28 @@ namespace osu.Desktop
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
private static void setupVelopack()
|
||||||
private static void setupSquirrel()
|
|
||||||
{
|
{
|
||||||
SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
|
if (OsuGameDesktop.IsPackageManaged)
|
||||||
{
|
{
|
||||||
tools.CreateShortcutForThisExe();
|
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
|
||||||
tools.CreateUninstallerRegistryEntry();
|
return;
|
||||||
WindowsAssociationManager.InstallAssociations();
|
}
|
||||||
}, onAppUpdate: (_, tools) =>
|
|
||||||
{
|
var app = VelopackApp.Build();
|
||||||
tools.CreateUninstallerRegistryEntry();
|
|
||||||
WindowsAssociationManager.UpdateAssociations();
|
if (OperatingSystem.IsWindows())
|
||||||
}, onAppUninstall: (_, tools) =>
|
configureWindows(app);
|
||||||
{
|
|
||||||
tools.RemoveShortcutForThisExe();
|
app.Run();
|
||||||
tools.RemoveUninstallerRegistryEntry();
|
}
|
||||||
WindowsAssociationManager.UninstallAssociations();
|
|
||||||
}, onEveryRun: (_, _, _) =>
|
[SupportedOSPlatform("windows")]
|
||||||
{
|
private static void configureWindows(VelopackApp app)
|
||||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
{
|
||||||
// causes the right-click context menu to function incorrectly.
|
app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
||||||
//
|
app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
||||||
// This may turn out to be non-required after an alternative solution is implemented.
|
app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
||||||
// see https://github.com/clowd/Clowd.Squirrel/issues/24
|
|
||||||
// tools.SetProcessAppUserModelId();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,180 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Game;
|
|
||||||
using osu.Game.Overlays;
|
|
||||||
using osu.Game.Overlays.Notifications;
|
|
||||||
using osu.Game.Screens.Play;
|
|
||||||
using Squirrel.SimpleSplat;
|
|
||||||
using Squirrel.Sources;
|
|
||||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
|
||||||
using UpdateManager = osu.Game.Updater.UpdateManager;
|
|
||||||
|
|
||||||
namespace osu.Desktop.Updater
|
|
||||||
{
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
public partial class SquirrelUpdateManager : UpdateManager
|
|
||||||
{
|
|
||||||
private Squirrel.UpdateManager? updateManager;
|
|
||||||
private INotificationOverlay notificationOverlay = null!;
|
|
||||||
|
|
||||||
public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited();
|
|
||||||
|
|
||||||
private static readonly Logger logger = Logger.GetLogger("updater");
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether an update has been downloaded but not yet applied.
|
|
||||||
/// </summary>
|
|
||||||
private bool updatePending;
|
|
||||||
|
|
||||||
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private OsuGameBase game { get; set; } = null!;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private ILocalUserPlayInfo? localUserInfo { get; set; }
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(INotificationOverlay notifications)
|
|
||||||
{
|
|
||||||
notificationOverlay = notifications;
|
|
||||||
|
|
||||||
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null)
|
|
||||||
{
|
|
||||||
// should we schedule a retry on completion of this check?
|
|
||||||
bool scheduleRecheck = true;
|
|
||||||
|
|
||||||
const string? github_token = null; // TODO: populate.
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Avoid any kind of update checking while gameplay is running.
|
|
||||||
if (localUserInfo?.IsPlaying.Value == true)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer");
|
|
||||||
|
|
||||||
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (info.ReleasesToApply.Count == 0)
|
|
||||||
{
|
|
||||||
if (updatePending)
|
|
||||||
{
|
|
||||||
// the user may have dismissed the completion notice, so show it again.
|
|
||||||
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
|
||||||
{
|
|
||||||
Activated = () =>
|
|
||||||
{
|
|
||||||
restartToApplyUpdate();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no updates available. bail and retry later.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleRecheck = false;
|
|
||||||
|
|
||||||
if (notification == null)
|
|
||||||
{
|
|
||||||
notification = new UpdateProgressNotification
|
|
||||||
{
|
|
||||||
CompletionClickAction = restartToApplyUpdate,
|
|
||||||
};
|
|
||||||
|
|
||||||
Schedule(() => notificationOverlay.Post(notification));
|
|
||||||
}
|
|
||||||
|
|
||||||
notification.StartDownload();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
|
||||||
|
|
||||||
notification.StartInstall();
|
|
||||||
|
|
||||||
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
|
||||||
|
|
||||||
notification.State = ProgressNotificationState.Completed;
|
|
||||||
updatePending = true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
if (useDeltaPatching)
|
|
||||||
{
|
|
||||||
logger.Add(@"delta patching failed; will attempt full download!");
|
|
||||||
|
|
||||||
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
|
|
||||||
// try again without deltas.
|
|
||||||
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// In the case of an error, a separate notification will be displayed.
|
|
||||||
notification.FailDownload();
|
|
||||||
Logger.Error(e, @"update failed!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
|
|
||||||
scheduleRecheck = true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (scheduleRecheck)
|
|
||||||
{
|
|
||||||
// check again in 30 minutes.
|
|
||||||
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool restartToApplyUpdate()
|
|
||||||
{
|
|
||||||
PrepareUpdateAsync()
|
|
||||||
.ContinueWith(_ => Schedule(() => game.AttemptExit()));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
|
||||||
{
|
|
||||||
base.Dispose(isDisposing);
|
|
||||||
updateManager?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SquirrelLogger : ILogger, IDisposable
|
|
||||||
{
|
|
||||||
public LogLevel Level { get; set; } = LogLevel.Info;
|
|
||||||
|
|
||||||
public void Write(string message, LogLevel logLevel)
|
|
||||||
{
|
|
||||||
if (logLevel < Level)
|
|
||||||
return;
|
|
||||||
|
|
||||||
logger.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
139
osu.Desktop/Updater/VelopackUpdateManager.cs
Normal file
139
osu.Desktop/Updater/VelopackUpdateManager.cs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Game;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using Velopack;
|
||||||
|
using Velopack.Sources;
|
||||||
|
|
||||||
|
namespace osu.Desktop.Updater
|
||||||
|
{
|
||||||
|
public partial class VelopackUpdateManager : Game.Updater.UpdateManager
|
||||||
|
{
|
||||||
|
private readonly UpdateManager updateManager;
|
||||||
|
private INotificationOverlay notificationOverlay = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuGameBase game { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ILocalUserPlayInfo? localUserInfo { get; set; }
|
||||||
|
|
||||||
|
private UpdateInfo? pendingUpdate;
|
||||||
|
|
||||||
|
public VelopackUpdateManager()
|
||||||
|
{
|
||||||
|
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions
|
||||||
|
{
|
||||||
|
AllowVersionDowngrade = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(INotificationOverlay notifications)
|
||||||
|
{
|
||||||
|
notificationOverlay = notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
private async Task<bool> checkForUpdateAsync(UpdateProgressNotification? notification = null)
|
||||||
|
{
|
||||||
|
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
|
||||||
|
bool scheduleRecheck = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Avoid any kind of update checking while gameplay is running.
|
||||||
|
if (localUserInfo?.IsPlaying.Value == true)
|
||||||
|
{
|
||||||
|
scheduleRecheck = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
|
||||||
|
// Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975).
|
||||||
|
if (pendingUpdate != null)
|
||||||
|
{
|
||||||
|
// If there is an update pending restart, show the notification to restart again.
|
||||||
|
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
||||||
|
{
|
||||||
|
Activated = () =>
|
||||||
|
{
|
||||||
|
restartToApplyUpdate();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// No update is available. We'll check again later.
|
||||||
|
if (pendingUpdate == null)
|
||||||
|
{
|
||||||
|
scheduleRecheck = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An update is found, let's notify the user and start downloading it.
|
||||||
|
if (notification == null)
|
||||||
|
{
|
||||||
|
notification = new UpdateProgressNotification
|
||||||
|
{
|
||||||
|
CompletionClickAction = restartToApplyUpdate,
|
||||||
|
};
|
||||||
|
|
||||||
|
Schedule(() => notificationOverlay.Post(notification));
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.StartDownload();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||||
|
|
||||||
|
notification.State = ProgressNotificationState.Completed;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// In the case of an error, a separate notification will be displayed.
|
||||||
|
scheduleRecheck = true;
|
||||||
|
notification.FailDownload();
|
||||||
|
Logger.Error(e, @"update failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
|
||||||
|
scheduleRecheck = true;
|
||||||
|
Logger.Log($@"update check failed ({e.Message})");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (scheduleRecheck)
|
||||||
|
{
|
||||||
|
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool restartToApplyUpdate()
|
||||||
|
{
|
||||||
|
// TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665).
|
||||||
|
// Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart.
|
||||||
|
updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease);
|
||||||
|
Schedule(() => game.AttemptExit());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
|
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
|
||||||
<AssemblyName>osu!</AssemblyName>
|
<AssemblyName>osu!</AssemblyName>
|
||||||
|
<AssemblyTitle>osu!(lazer)</AssemblyTitle>
|
||||||
<Title>osu!</Title>
|
<Title>osu!</Title>
|
||||||
<Product>osu!(lazer)</Product>
|
<Product>osu!(lazer)</Product>
|
||||||
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
||||||
@ -23,9 +24,9 @@
|
|||||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" />
|
|
||||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
|
<PackageReference Include="Velopack" Version="0.0.598-g933b2ab" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -19,20 +18,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin)
|
public void TestLegacyHUDComboCounterNotExistent([Values] bool withModifiedSkin)
|
||||||
{
|
{
|
||||||
if (withModifiedSkin)
|
if (withModifiedSkin)
|
||||||
{
|
{
|
||||||
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
|
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
|
||||||
AddStep("update target", () => Player.ChildrenOfType<SkinComponentsContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
AddStep("update target", () => Player.ChildrenOfType<SkinnableContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
||||||
AddStep("exit player", () => Player.Exit());
|
AddStep("exit player", () => Player.Exit());
|
||||||
CreateTest();
|
CreateTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
AddAssert("legacy HUD combo counter hidden", () =>
|
AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType<LegacyDefaultComboCounter>().Any());
|
||||||
{
|
|
||||||
return Player.ChildrenOfType<LegacyComboCounter>().All(c => c.ChildrenOfType<Container>().Single().Alpha == 0f);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,7 +248,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
|
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
|
||||||
AddStep("catch fruit", () => attemptCatch(new Fruit()));
|
AddStep("catch fruit", () => attemptCatch(new Fruit()));
|
||||||
AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
|
AddAssert("correct hit lighting colour",
|
||||||
|
() => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -259,6 +260,16 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
|
AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAllExplodedObjectsAtUniquePositions()
|
||||||
|
{
|
||||||
|
AddStep("catch normal fruit", () => attemptCatch(new Fruit()));
|
||||||
|
AddStep("catch normal fruit", () => attemptCatch(new Fruit { IndexInBeatmap = 2, LastInCombo = true }));
|
||||||
|
AddAssert("two fruit at distinct x coordinates",
|
||||||
|
() => this.ChildrenOfType<CaughtFruit>().Select(f => f.DrawPosition.X).Distinct(),
|
||||||
|
() => Has.Exactly(2).Items);
|
||||||
|
}
|
||||||
|
|
||||||
private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
|
private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
|
||||||
|
|
||||||
private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state);
|
private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state);
|
||||||
|
@ -254,5 +254,7 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
|
|
||||||
return adjustedDifficulty;
|
return adjustedDifficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool EditorShowScrollSpeed => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch
|
namespace osu.Game.Rulesets.Catch
|
||||||
{
|
{
|
||||||
public class CatchSkinComponentLookup : GameplaySkinComponentLookup<CatchSkinComponents>
|
public class CatchSkinComponentLookup : SkinComponentLookup<CatchSkinComponents>
|
||||||
{
|
{
|
||||||
public CatchSkinComponentLookup(CatchSkinComponents component)
|
public CatchSkinComponentLookup(CatchSkinComponents component)
|
||||||
: base(component)
|
: base(component)
|
||||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
{
|
{
|
||||||
public class CatchDifficultyCalculator : DifficultyCalculator
|
public class CatchDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const double star_scaling_factor = 0.153;
|
private const double difficulty_multiplier = 4.59;
|
||||||
|
|
||||||
private float halfCatcherWidth;
|
private float halfCatcherWidth;
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
|
|
||||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
|||||||
private const float normalized_hitobject_radius = 41.0f;
|
private const float normalized_hitobject_radius = 41.0f;
|
||||||
private const double direction_change_bonus = 21.0;
|
private const double direction_change_bonus = 21.0;
|
||||||
|
|
||||||
protected override double SkillMultiplier => 900;
|
protected override double SkillMultiplier => 1;
|
||||||
protected override double StrainDecayBase => 0.2;
|
protected override double StrainDecayBase => 0.2;
|
||||||
|
|
||||||
protected override double DecayWeight => 0.94;
|
protected override double DecayWeight => 0.94;
|
||||||
|
@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||||
|
|
||||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||||
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
|
|
||||||
|
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
|
||||||
|
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
|
||||||
|
|
||||||
return actualDistance / expectedDistance;
|
return actualDistance / expectedDistance;
|
||||||
}
|
}
|
||||||
|
@ -21,11 +21,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
|
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
|
||||||
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
|
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
|
||||||
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
|
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
|
||||||
|
public Vector2 DisplayPosition => DrawPosition;
|
||||||
public Vector2 DisplaySize => Size * Scale;
|
public Vector2 DisplaySize => Size * Scale;
|
||||||
|
|
||||||
public float DisplayRotation => Rotation;
|
public float DisplayRotation => Rotation;
|
||||||
|
|
||||||
public double DisplayStartTime => HitObject.StartTime;
|
public double DisplayStartTime => HitObject.StartTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -44,19 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
|
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copies the hit object visual state from another <see cref="IHasCatchObjectState"/> object.
|
|
||||||
/// </summary>
|
|
||||||
public virtual void CopyStateFrom(IHasCatchObjectState objectState)
|
|
||||||
{
|
|
||||||
HitObject = objectState.HitObject;
|
|
||||||
Scale = Vector2.Divide(objectState.DisplaySize, Size);
|
|
||||||
Rotation = objectState.DisplayRotation;
|
|
||||||
AccentColour.Value = objectState.AccentColour.Value;
|
|
||||||
HyperDash.Value = objectState.HyperDash.Value;
|
|
||||||
IndexInBeatmap.Value = objectState.IndexInBeatmap.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void FreeAfterUse()
|
protected override void FreeAfterUse()
|
||||||
{
|
{
|
||||||
ClearTransforms();
|
ClearTransforms();
|
||||||
@ -64,5 +49,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
|
|
||||||
base.FreeAfterUse();
|
base.FreeAfterUse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RestoreState(CatchObjectState state)
|
||||||
|
{
|
||||||
|
HitObject = state.HitObject;
|
||||||
|
AccentColour.Value = state.AccentColour;
|
||||||
|
HyperDash.Value = state.HyperDash;
|
||||||
|
IndexInBeatmap.Value = state.IndexInBeatmap;
|
||||||
|
Position = state.DisplayPosition;
|
||||||
|
Scale = Vector2.Divide(state.DisplaySize, Size);
|
||||||
|
Rotation = state.DisplayRotation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly Container ScalingContainer;
|
protected readonly Container ScalingContainer;
|
||||||
|
|
||||||
|
public Vector2 DisplayPosition => DrawPosition;
|
||||||
|
|
||||||
public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
|
public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
|
||||||
|
|
||||||
public float DisplayRotation => ScalingContainer.Rotation;
|
public float DisplayRotation => ScalingContainer.Rotation;
|
||||||
@ -95,5 +97,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
|
|
||||||
base.OnFree();
|
base.OnFree();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RestoreState(CatchObjectState state) => throw new NotSupportedException("Cannot restore state into a drawable catch hitobject.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,17 +13,35 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
|||||||
public interface IHasCatchObjectState
|
public interface IHasCatchObjectState
|
||||||
{
|
{
|
||||||
PalpableCatchHitObject HitObject { get; }
|
PalpableCatchHitObject HitObject { get; }
|
||||||
|
|
||||||
double DisplayStartTime { get; }
|
|
||||||
|
|
||||||
Bindable<Color4> AccentColour { get; }
|
Bindable<Color4> AccentColour { get; }
|
||||||
|
|
||||||
Bindable<bool> HyperDash { get; }
|
Bindable<bool> HyperDash { get; }
|
||||||
|
|
||||||
Bindable<int> IndexInBeatmap { get; }
|
Bindable<int> IndexInBeatmap { get; }
|
||||||
|
double DisplayStartTime { get; }
|
||||||
|
Vector2 DisplayPosition { get; }
|
||||||
Vector2 DisplaySize { get; }
|
Vector2 DisplaySize { get; }
|
||||||
|
|
||||||
float DisplayRotation { get; }
|
float DisplayRotation { get; }
|
||||||
|
|
||||||
|
void RestoreState(CatchObjectState state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class HasCatchObjectStateExtensions
|
||||||
|
{
|
||||||
|
public static CatchObjectState SaveState(this IHasCatchObjectState target) => new CatchObjectState(
|
||||||
|
target.HitObject,
|
||||||
|
target.AccentColour.Value,
|
||||||
|
target.HyperDash.Value,
|
||||||
|
target.IndexInBeatmap.Value,
|
||||||
|
target.DisplayPosition,
|
||||||
|
target.DisplaySize,
|
||||||
|
target.DisplayRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct CatchObjectState(
|
||||||
|
PalpableCatchHitObject HitObject,
|
||||||
|
Color4 AccentColour,
|
||||||
|
bool HyperDash,
|
||||||
|
int IndexInBeatmap,
|
||||||
|
Vector2 DisplayPosition,
|
||||||
|
Vector2 DisplaySize,
|
||||||
|
float DisplayRotation);
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,12 @@ namespace osu.Game.Rulesets.Catch.Replays
|
|||||||
{
|
{
|
||||||
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
|
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
|
||||||
|
|
||||||
|
private readonly float halfCatcherWidth;
|
||||||
|
|
||||||
public CatchAutoGenerator(IBeatmap beatmap)
|
public CatchAutoGenerator(IBeatmap beatmap)
|
||||||
: base(beatmap)
|
: base(beatmap)
|
||||||
{
|
{
|
||||||
|
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void GenerateFrames()
|
protected override void GenerateFrames()
|
||||||
@ -47,10 +50,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
|||||||
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
|
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
|
||||||
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
|
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
|
||||||
|
|
||||||
// todo: get correct catcher size, based on difficulty CS.
|
if (lastPosition - halfCatcherWidth < h.EffectiveX && lastPosition + halfCatcherWidth > h.EffectiveX)
|
||||||
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
|
|
||||||
|
|
||||||
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
|
|
||||||
{
|
{
|
||||||
// we are already in the correct range.
|
// we are already in the correct range.
|
||||||
lastTime = h.StartTime;
|
lastTime = h.StartTime;
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||||
@ -28,76 +28,94 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
{
|
{
|
||||||
if (lookup is SkinComponentsContainerLookup containerLookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
switch (containerLookup.Target)
|
case GlobalSkinnableContainerLookup containerLookup:
|
||||||
{
|
// Only handle per ruleset defaults here.
|
||||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
if (containerLookup.Ruleset == null)
|
||||||
var components = base.GetDrawableComponent(lookup) as Container;
|
return base.GetDrawableComponent(lookup);
|
||||||
|
|
||||||
if (providesComboCounter && components != null)
|
|
||||||
{
|
|
||||||
// catch may provide its own combo counter; hide the default.
|
|
||||||
// todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed.
|
|
||||||
foreach (var legacyComboCounter in components.OfType<LegacyComboCounter>())
|
|
||||||
legacyComboCounter.HiddenByRulesetImplementation = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return components;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lookup is CatchSkinComponentLookup catchSkinComponent)
|
|
||||||
{
|
|
||||||
switch (catchSkinComponent.Component)
|
|
||||||
{
|
|
||||||
case CatchSkinComponents.Fruit:
|
|
||||||
if (hasPear)
|
|
||||||
return new LegacyFruitPiece();
|
|
||||||
|
|
||||||
|
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||||
|
if (!IsProvidingLegacyResources)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case CatchSkinComponents.Banana:
|
// Our own ruleset components default.
|
||||||
if (GetTexture("fruit-bananas") != null)
|
switch (containerLookup.Lookup)
|
||||||
return new LegacyBananaPiece();
|
{
|
||||||
|
case GlobalSkinnableContainers.MainHUDComponents:
|
||||||
|
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
|
||||||
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
|
{
|
||||||
|
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||||
|
|
||||||
return null;
|
if (keyCounter != null)
|
||||||
|
{
|
||||||
|
// set the anchor to top right so that it won't squash to the return button to the top
|
||||||
|
keyCounter.Anchor = Anchor.CentreRight;
|
||||||
|
keyCounter.Origin = Anchor.TopRight;
|
||||||
|
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new LegacyKeyCounterDisplay(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case CatchSkinComponents.Droplet:
|
return null;
|
||||||
if (GetTexture("fruit-drop") != null)
|
|
||||||
return new LegacyDropletPiece();
|
|
||||||
|
|
||||||
return null;
|
case CatchSkinComponentLookup catchSkinComponent:
|
||||||
|
switch (catchSkinComponent.Component)
|
||||||
|
{
|
||||||
|
case CatchSkinComponents.Fruit:
|
||||||
|
if (hasPear)
|
||||||
|
return new LegacyFruitPiece();
|
||||||
|
|
||||||
case CatchSkinComponents.Catcher:
|
return null;
|
||||||
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
|
||||||
|
|
||||||
if (version < 2.3m)
|
case CatchSkinComponents.Banana:
|
||||||
{
|
if (GetTexture("fruit-bananas") != null)
|
||||||
if (hasOldStyleCatcherSprite())
|
return new LegacyBananaPiece();
|
||||||
return new LegacyCatcherOld();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNewStyleCatcherSprite())
|
return null;
|
||||||
return new LegacyCatcherNew();
|
|
||||||
|
|
||||||
return null;
|
case CatchSkinComponents.Droplet:
|
||||||
|
if (GetTexture("fruit-drop") != null)
|
||||||
|
return new LegacyDropletPiece();
|
||||||
|
|
||||||
case CatchSkinComponents.CatchComboCounter:
|
return null;
|
||||||
if (providesComboCounter)
|
|
||||||
return new LegacyCatchComboCounter();
|
|
||||||
|
|
||||||
return null;
|
case CatchSkinComponents.Catcher:
|
||||||
|
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
|
||||||
|
|
||||||
case CatchSkinComponents.HitExplosion:
|
if (version < 2.3m)
|
||||||
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
|
{
|
||||||
return new LegacyHitExplosion();
|
if (hasOldStyleCatcherSprite())
|
||||||
|
return new LegacyCatcherOld();
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
if (hasNewStyleCatcherSprite())
|
||||||
|
return new LegacyCatcherNew();
|
||||||
|
|
||||||
default:
|
return null;
|
||||||
throw new UnsupportedSkinComponentException(lookup);
|
|
||||||
}
|
case CatchSkinComponents.CatchComboCounter:
|
||||||
|
if (providesComboCounter)
|
||||||
|
return new LegacyCatchComboCounter();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case CatchSkinComponents.HitExplosion:
|
||||||
|
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
|
||||||
|
return new LegacyHitExplosion();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new UnsupportedSkinComponentException(lookup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.GetDrawableComponent(lookup);
|
return base.GetDrawableComponent(lookup);
|
||||||
|
@ -85,9 +85,25 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
|
|
||||||
protected void SetTexture(Texture? texture, Texture? overlayTexture)
|
protected void SetTexture(Texture? texture, Texture? overlayTexture)
|
||||||
{
|
{
|
||||||
colouredSprite.Texture = texture;
|
// Sizes are reset due to an arguable osu!framework bug where Sprite retains the size of the first set texture.
|
||||||
overlaySprite.Texture = overlayTexture;
|
|
||||||
hyperSprite.Texture = texture;
|
if (colouredSprite.Texture != texture)
|
||||||
|
{
|
||||||
|
colouredSprite.Size = Vector2.Zero;
|
||||||
|
colouredSprite.Texture = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlaySprite.Texture != overlayTexture)
|
||||||
|
{
|
||||||
|
overlaySprite.Size = Vector2.Zero;
|
||||||
|
overlaySprite.Texture = overlayTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hyperSprite.Texture != texture)
|
||||||
|
{
|
||||||
|
hyperSprite.Size = Vector2.Zero;
|
||||||
|
hyperSprite.Texture = texture;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -362,7 +363,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
if (caughtObject == null) return;
|
if (caughtObject == null) return;
|
||||||
|
|
||||||
caughtObject.CopyStateFrom(drawableObject);
|
caughtObject.RestoreState(drawableObject.SaveState());
|
||||||
caughtObject.Anchor = Anchor.TopCentre;
|
caughtObject.Anchor = Anchor.TopCentre;
|
||||||
caughtObject.Position = position;
|
caughtObject.Position = position;
|
||||||
caughtObject.Scale *= caught_fruit_scale_adjust;
|
caughtObject.Scale *= caught_fruit_scale_adjust;
|
||||||
@ -411,41 +412,50 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private CaughtObject getDroppedObject(CaughtObject caughtObject)
|
private CaughtObject getDroppedObject(CatchObjectState state)
|
||||||
{
|
{
|
||||||
var droppedObject = getCaughtObject(caughtObject.HitObject);
|
var droppedObject = getCaughtObject(state.HitObject);
|
||||||
Debug.Assert(droppedObject != null);
|
Debug.Assert(droppedObject != null);
|
||||||
|
|
||||||
droppedObject.CopyStateFrom(caughtObject);
|
droppedObject.RestoreState(state);
|
||||||
droppedObject.Anchor = Anchor.TopLeft;
|
droppedObject.Anchor = Anchor.TopLeft;
|
||||||
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget);
|
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(state.DisplayPosition, droppedObjectTarget);
|
||||||
|
|
||||||
return droppedObject;
|
return droppedObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearPlate(DroppedObjectAnimation animation)
|
private void clearPlate(DroppedObjectAnimation animation)
|
||||||
{
|
{
|
||||||
var caughtObjects = caughtObjectContainer.Children.ToArray();
|
int caughtCount = caughtObjectContainer.Children.Count;
|
||||||
|
CatchObjectState[] states = ArrayPool<CatchObjectState>.Shared.Rent(caughtCount);
|
||||||
|
|
||||||
caughtObjectContainer.Clear(false);
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < caughtCount; i++)
|
||||||
|
states[i] = caughtObjectContainer.Children[i].SaveState();
|
||||||
|
|
||||||
// Use the already returned PoolableDrawables for new objects
|
caughtObjectContainer.Clear(false);
|
||||||
var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
|
|
||||||
|
|
||||||
droppedObjectTarget.AddRange(droppedObjects);
|
for (int i = 0; i < caughtCount; i++)
|
||||||
|
{
|
||||||
foreach (var droppedObject in droppedObjects)
|
CaughtObject obj = getDroppedObject(states[i]);
|
||||||
applyDropAnimation(droppedObject, animation);
|
droppedObjectTarget.Add(obj);
|
||||||
|
applyDropAnimation(obj, animation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<CatchObjectState>.Shared.Return(states);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
|
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
|
||||||
{
|
{
|
||||||
|
CatchObjectState state = caughtObject.SaveState();
|
||||||
caughtObjectContainer.Remove(caughtObject, false);
|
caughtObjectContainer.Remove(caughtObject, false);
|
||||||
|
|
||||||
var droppedObject = getDroppedObject(caughtObject);
|
var droppedObject = getDroppedObject(state);
|
||||||
|
|
||||||
droppedObjectTarget.Add(droppedObject);
|
droppedObjectTarget.Add(droppedObject);
|
||||||
|
|
||||||
applyDropAnimation(droppedObject, animation);
|
applyDropAnimation(droppedObject, animation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
if (Catcher.Dashing || Catcher.HyperDashing)
|
if (Catcher.Dashing || Catcher.HyperDashing)
|
||||||
{
|
{
|
||||||
double generationInterval = Catcher.HyperDashing ? 25 : 50;
|
const double trail_generation_interval = 16;
|
||||||
|
|
||||||
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
|
if (Time.Current - catcherTrails.LastDashTrailTime >= trail_generation_interval)
|
||||||
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
{
|
{
|
||||||
[TestCase(ManiaAction.Key1)]
|
[TestCase(ManiaAction.Key1)]
|
||||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||||
[TestCase(ManiaAction.Special1)]
|
[TestCase(ManiaAction.Key5)]
|
||||||
[TestCase(ManiaAction.Key8)]
|
[TestCase(ManiaAction.Key9)]
|
||||||
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
|
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
|
||||||
{
|
{
|
||||||
var beatmap = new ManiaBeatmap(new StageDefinition(9));
|
var beatmap = new ManiaBeatmap(new StageDefinition(9));
|
||||||
@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
|
|
||||||
[TestCase(ManiaAction.Key1)]
|
[TestCase(ManiaAction.Key1)]
|
||||||
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
|
||||||
[TestCase(ManiaAction.Special1)]
|
[TestCase(ManiaAction.Key3)]
|
||||||
[TestCase(ManiaAction.Special2)]
|
|
||||||
[TestCase(ManiaAction.Special1, ManiaAction.Special2)]
|
|
||||||
[TestCase(ManiaAction.Special1, ManiaAction.Key5)]
|
|
||||||
[TestCase(ManiaAction.Key8)]
|
[TestCase(ManiaAction.Key8)]
|
||||||
|
[TestCase(ManiaAction.Key3, ManiaAction.Key8)]
|
||||||
|
[TestCase(ManiaAction.Key3, ManiaAction.Key6)]
|
||||||
|
[TestCase(ManiaAction.Key10)]
|
||||||
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
|
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
|
||||||
{
|
{
|
||||||
var beatmap = new ManiaBeatmap(new StageDefinition(5));
|
var beatmap = new ManiaBeatmap(new StageDefinition(5));
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene
|
public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene
|
||||||
{
|
{
|
||||||
[Cached(Type = typeof(IScrollingInfo))]
|
[Cached(Type = typeof(IScrollingInfo))]
|
||||||
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
|
protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly StageDefinition stage = new StageDefinition(4);
|
private readonly StageDefinition stage = new StageDefinition(4);
|
||||||
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
|
|
||||||
protected ManiaSkinnableTestScene()
|
protected ManiaSkinnableTestScene()
|
||||||
{
|
{
|
||||||
scrollingInfo.Direction.Value = ScrollingDirection.Down;
|
ScrollingInfo.Direction.Value = ScrollingDirection.Down;
|
||||||
|
|
||||||
Add(new Box
|
Add(new Box
|
||||||
{
|
{
|
||||||
@ -43,16 +43,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestScrollingDown()
|
public void TestScrollingDown()
|
||||||
{
|
{
|
||||||
AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down);
|
AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestScrollingUp()
|
public void TestScrollingUp()
|
||||||
{
|
{
|
||||||
AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up);
|
AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestScrollingInfo : IScrollingInfo
|
protected class TestScrollingInfo : IScrollingInfo
|
||||||
{
|
{
|
||||||
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||||
|
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Mania.Skinning.Argon;
|
||||||
|
using osu.Game.Rulesets.Mania.Skinning.Legacy;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||||
|
{
|
||||||
|
public partial class TestSceneComboCounter : ManiaSkinnableTestScene
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDisplay()
|
||||||
|
{
|
||||||
|
setup(Anchor.Centre);
|
||||||
|
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20);
|
||||||
|
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAnchorOrigin()
|
||||||
|
{
|
||||||
|
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||||
|
setup(Anchor.TopCentre, 20);
|
||||||
|
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||||
|
check(Anchor.BottomCentre, -20);
|
||||||
|
|
||||||
|
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||||
|
setup(Anchor.BottomCentre, -20);
|
||||||
|
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||||
|
check(Anchor.TopCentre, 20);
|
||||||
|
|
||||||
|
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||||
|
setup(Anchor.Centre, 20);
|
||||||
|
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||||
|
check(Anchor.Centre, 20);
|
||||||
|
|
||||||
|
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||||
|
setup(Anchor.Centre, -20);
|
||||||
|
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||||
|
check(Anchor.Centre, -20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setup(Anchor anchor, float y = 0)
|
||||||
|
{
|
||||||
|
AddStep($"setup {anchor} {y}", () => SetContents(s =>
|
||||||
|
{
|
||||||
|
var container = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (s is ArgonSkin)
|
||||||
|
container.Add(new ArgonManiaComboCounter());
|
||||||
|
else if (s is LegacySkin)
|
||||||
|
container.Add(new LegacyManiaComboCounter());
|
||||||
|
else
|
||||||
|
container.Add(new LegacyManiaComboCounter());
|
||||||
|
|
||||||
|
container.Child.Anchor = anchor;
|
||||||
|
container.Child.Origin = Anchor.Centre;
|
||||||
|
container.Child.Y = y;
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void check(Anchor anchor, float y)
|
||||||
|
{
|
||||||
|
AddAssert($"check {anchor} {y}", () =>
|
||||||
|
{
|
||||||
|
foreach (var combo in this.ChildrenOfType<ISerialisableDrawable>())
|
||||||
|
{
|
||||||
|
var drawableCombo = (Drawable)combo;
|
||||||
|
if (drawableCombo.Anchor != anchor || drawableCombo.Y != y)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,14 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
AddStep("Show " + result.GetDescription(), () =>
|
AddStep("Show " + result.GetDescription(), () =>
|
||||||
{
|
{
|
||||||
SetContents(_ =>
|
SetContents(_ =>
|
||||||
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
|
{
|
||||||
{
|
var drawableManiaJudgement = new DrawableManiaJudgement
|
||||||
Type = result
|
|
||||||
}, null)
|
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
drawableManiaJudgement.Apply(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
|
||||||
|
{
|
||||||
|
Type = result
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
return drawableManiaJudgement;
|
||||||
|
});
|
||||||
|
|
||||||
// for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value
|
// for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value
|
||||||
// (see `LegacyManiaJudgementPiece.load()`).
|
// (see `LegacyManiaJudgementPiece.load()`).
|
||||||
|
@ -3,15 +3,22 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||||
{
|
{
|
||||||
public partial class TestScenePlayfield : ManiaSkinnableTestScene
|
public partial class TestScenePlayfield : ManiaSkinnableTestScene
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
|
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
|
||||||
|
|
||||||
private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
|
private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -29,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
Child = new ManiaPlayfield(stageDefinitions)
|
Child = new ManiaPlayfield(stageDefinitions)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
|
||||||
|
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(2)]
|
[TestCase(2)]
|
||||||
@ -54,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
|
||||||
|
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IBeatmap CreateBeatmapForSkinProvider()
|
protected override IBeatmap CreateBeatmapForSkinProvider()
|
||||||
|
@ -14,12 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
|||||||
{
|
{
|
||||||
SetContents(_ =>
|
SetContents(_ =>
|
||||||
{
|
{
|
||||||
ManiaAction normalAction = ManiaAction.Key1;
|
ManiaAction action = ManiaAction.Key1;
|
||||||
ManiaAction specialAction = ManiaAction.Special1;
|
|
||||||
|
|
||||||
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
|
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
|
||||||
{
|
{
|
||||||
Child = new Stage(0, new StageDefinition(4), ref normalAction, ref specialAction)
|
Child = new Stage(0, new StageDefinition(4), ref action)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||||
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
|
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
|
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
|
||||||
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
|
||||||
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
|
||||||
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
|
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
|
||||||
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
|
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Mods;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneManiaPlayerLegacySkin : LegacySkinPlayerTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||||
|
|
||||||
|
// play with a converted beatmap to allow dual stages mod to work.
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(new RulesetInfo());
|
||||||
|
|
||||||
|
protected override bool HasCustomSteps => true;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleStage()
|
||||||
|
{
|
||||||
|
AddStep("Load single stage", LoadPlayer);
|
||||||
|
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDualStage()
|
||||||
|
{
|
||||||
|
AddStep("Load dual stage", () => LoadPlayer(new Mod[] { new ManiaModDualStages() }));
|
||||||
|
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -131,9 +131,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
|
|
||||||
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)
|
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)
|
||||||
{
|
{
|
||||||
var specialAction = ManiaAction.Special1;
|
var stage = new Stage(0, new StageDefinition(2), ref action);
|
||||||
|
|
||||||
var stage = new Stage(0, new StageDefinition(2), ref action, ref specialAction);
|
|
||||||
stages.Add(stage);
|
stages.Add(stage);
|
||||||
|
|
||||||
return new ScrollingTestContainer(direction)
|
return new ScrollingTestContainer(direction)
|
||||||
|
@ -6,7 +6,6 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Game.Audio;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -271,7 +270,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
Duration = endTimeData.Duration,
|
Duration = endTimeData.Duration,
|
||||||
Column = column,
|
Column = column,
|
||||||
Samples = HitObject.Samples,
|
Samples = HitObject.Samples,
|
||||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? defaultNodeSamples
|
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (HitObject is IHasXPosition)
|
else if (HitObject is IHasXPosition)
|
||||||
@ -286,16 +285,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
|
|
||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <remarks>
|
|
||||||
/// osu!mania-specific beatmaps in stable only play samples at the start of the hold note.
|
|
||||||
/// </remarks>
|
|
||||||
private List<IList<HitSampleInfo>> defaultNodeSamples
|
|
||||||
=> new List<IList<HitSampleInfo>>
|
|
||||||
{
|
|
||||||
HitObject.Samples,
|
|
||||||
new List<HitSampleInfo>()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
{
|
{
|
||||||
public class ManiaDifficultyCalculator : DifficultyCalculator
|
public class ManiaDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const double star_scaling_factor = 0.018;
|
private const double difficulty_multiplier = 0.018;
|
||||||
|
|
||||||
private readonly bool isForCurrentRuleset;
|
private readonly bool isForCurrentRuleset;
|
||||||
private readonly double originalOverallDifficulty;
|
private readonly double originalOverallDifficulty;
|
||||||
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
StarRating = skills[0].DifficultyValue() * difficulty_multiplier,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
// 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.
|
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||||
|
@ -38,9 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
scoreAccuracy = calculateCustomAccuracy();
|
scoreAccuracy = calculateCustomAccuracy();
|
||||||
|
|
||||||
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
|
double multiplier = 1.0;
|
||||||
// The specific number has no intrinsic meaning and can be adjusted as needed.
|
|
||||||
double multiplier = 8.0;
|
|
||||||
|
|
||||||
if (score.Mods.Any(m => m is ModNoFail))
|
if (score.Mods.Any(m => m is ModNoFail))
|
||||||
multiplier *= 0.75;
|
multiplier *= 0.75;
|
||||||
@ -59,9 +57,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
|
private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
|
||||||
{
|
{
|
||||||
double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
|
double difficultyValue = 8.0 * 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
|
* 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
|
* (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
|
||||||
|
|
||||||
return difficultyValue;
|
return difficultyValue;
|
||||||
}
|
}
|
||||||
|
@ -45,18 +45,15 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
LeftKeys = stage1LeftKeys,
|
LeftKeys = stage1LeftKeys,
|
||||||
RightKeys = stage1RightKeys,
|
RightKeys = stage1RightKeys,
|
||||||
SpecialKey = InputKey.V,
|
SpecialKey = InputKey.V,
|
||||||
SpecialAction = ManiaAction.Special1,
|
}.GenerateKeyBindingsFor(singleStageVariant);
|
||||||
NormalActionStart = ManiaAction.Key1
|
|
||||||
}.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal);
|
|
||||||
|
|
||||||
var stage2Bindings = new VariantMappingGenerator
|
var stage2Bindings = new VariantMappingGenerator
|
||||||
{
|
{
|
||||||
LeftKeys = stage2LeftKeys,
|
LeftKeys = stage2LeftKeys,
|
||||||
RightKeys = stage2RightKeys,
|
RightKeys = stage2RightKeys,
|
||||||
SpecialKey = InputKey.B,
|
SpecialKey = InputKey.B,
|
||||||
SpecialAction = ManiaAction.Special2,
|
ActionStart = (ManiaAction)singleStageVariant,
|
||||||
NormalActionStart = nextNormal
|
}.GenerateKeyBindingsFor(singleStageVariant);
|
||||||
}.GenerateKeyBindingsFor(singleStageVariant, out _);
|
|
||||||
|
|
||||||
return stage1Bindings.Concat(stage2Bindings);
|
return stage1Bindings.Concat(stage2Bindings);
|
||||||
}
|
}
|
||||||
|
@ -19,16 +19,8 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public enum ManiaAction
|
public enum ManiaAction
|
||||||
{
|
{
|
||||||
[Description("Special 1")]
|
|
||||||
Special1 = 1,
|
|
||||||
|
|
||||||
[Description("Special 2")]
|
|
||||||
Special2,
|
|
||||||
|
|
||||||
// This offsets the start value of normal keys in-case we add more special keys
|
|
||||||
// above at a later time, without breaking replays/configs.
|
|
||||||
[Description("Key 1")]
|
[Description("Key 1")]
|
||||||
Key1 = 10,
|
Key1,
|
||||||
|
|
||||||
[Description("Key 2")]
|
[Description("Key 2")]
|
||||||
Key2,
|
Key2,
|
||||||
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania
|
namespace osu.Game.Rulesets.Mania
|
||||||
{
|
{
|
||||||
public class ManiaSkinComponentLookup : GameplaySkinComponentLookup<ManiaSkinComponents>
|
public class ManiaSkinComponentLookup : SkinComponentLookup<ManiaSkinComponents>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="ManiaSkinComponentLookup"/>.
|
/// Creates a new <see cref="ManiaSkinComponentLookup"/>.
|
||||||
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
@ -91,6 +92,10 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
{
|
{
|
||||||
base.CreateNestedHitObjects(cancellationToken);
|
base.CreateNestedHitObjects(cancellationToken);
|
||||||
|
|
||||||
|
// Generally node samples will be populated by ManiaBeatmapConverter, but in a case like the editor they may not be.
|
||||||
|
// Ensure they are set to a sane default here.
|
||||||
|
NodeSamples ??= CreateDefaultNodeSamples(this);
|
||||||
|
|
||||||
AddNested(Head = new HeadNote
|
AddNested(Head = new HeadNote
|
||||||
{
|
{
|
||||||
StartTime = StartTime,
|
StartTime = StartTime,
|
||||||
@ -102,7 +107,7 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
{
|
{
|
||||||
StartTime = EndTime,
|
StartTime = EndTime,
|
||||||
Column = Column,
|
Column = Column,
|
||||||
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
|
Samples = GetNodeSamples(NodeSamples.Count - 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
AddNested(Body = new HoldNoteBody
|
AddNested(Body = new HoldNoteBody
|
||||||
@ -116,7 +121,20 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
|
||||||
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) =>
|
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) => nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
|
||||||
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create the default note samples for a hold note, based off their main sample.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// By default, osu!mania beatmaps in only play samples at the start of the hold note.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="obj">The object to use as a basis for the head sample.</param>
|
||||||
|
/// <returns>Defaults for assigning to <see cref="HoldNote.NodeSamples"/>.</returns>
|
||||||
|
public static List<IList<HitSampleInfo>> CreateDefaultNodeSamples(HitObject obj) => new List<IList<HitSampleInfo>>
|
||||||
|
{
|
||||||
|
obj.Samples,
|
||||||
|
new List<HitSampleInfo>(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,28 +17,9 @@ namespace osu.Game.Rulesets.Mania.Replays
|
|||||||
|
|
||||||
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
|
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
|
||||||
|
|
||||||
private readonly ManiaAction[] columnActions;
|
|
||||||
|
|
||||||
public ManiaAutoGenerator(ManiaBeatmap beatmap)
|
public ManiaAutoGenerator(ManiaBeatmap beatmap)
|
||||||
: base(beatmap)
|
: base(beatmap)
|
||||||
{
|
{
|
||||||
columnActions = new ManiaAction[Beatmap.TotalColumns];
|
|
||||||
|
|
||||||
var normalAction = ManiaAction.Key1;
|
|
||||||
var specialAction = ManiaAction.Special1;
|
|
||||||
int totalCounter = 0;
|
|
||||||
|
|
||||||
foreach (var stage in Beatmap.Stages)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < stage.Columns; i++)
|
|
||||||
{
|
|
||||||
if (stage.IsSpecialColumn(i))
|
|
||||||
columnActions[totalCounter] = specialAction++;
|
|
||||||
else
|
|
||||||
columnActions[totalCounter] = normalAction++;
|
|
||||||
totalCounter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void GenerateFrames()
|
protected override void GenerateFrames()
|
||||||
@ -57,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Replays
|
|||||||
switch (point)
|
switch (point)
|
||||||
{
|
{
|
||||||
case HitPoint:
|
case HitPoint:
|
||||||
actions.Add(columnActions[point.Column]);
|
actions.Add(ManiaAction.Key1 + point.Column);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ReleasePoint:
|
case ReleasePoint:
|
||||||
actions.Remove(columnActions[point.Column]);
|
actions.Remove(ManiaAction.Key1 + point.Column);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Replays.Legacy;
|
using osu.Game.Replays.Legacy;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
|
|
||||||
@ -27,118 +25,27 @@ namespace osu.Game.Rulesets.Mania.Replays
|
|||||||
|
|
||||||
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
|
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
|
||||||
{
|
{
|
||||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
var action = ManiaAction.Key1;
|
||||||
|
|
||||||
var normalAction = ManiaAction.Key1;
|
|
||||||
var specialAction = ManiaAction.Special1;
|
|
||||||
|
|
||||||
int activeColumns = (int)(legacyFrame.MouseX ?? 0);
|
int activeColumns = (int)(legacyFrame.MouseX ?? 0);
|
||||||
int counter = 0;
|
|
||||||
|
|
||||||
while (activeColumns > 0)
|
while (activeColumns > 0)
|
||||||
{
|
{
|
||||||
bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
|
|
||||||
|
|
||||||
if ((activeColumns & 1) > 0)
|
if ((activeColumns & 1) > 0)
|
||||||
Actions.Add(isSpecial ? specialAction : normalAction);
|
Actions.Add(action);
|
||||||
|
|
||||||
if (isSpecial)
|
action++;
|
||||||
specialAction++;
|
|
||||||
else
|
|
||||||
normalAction++;
|
|
||||||
|
|
||||||
counter++;
|
|
||||||
activeColumns >>= 1;
|
activeColumns >>= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
|
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
|
||||||
{
|
{
|
||||||
var maniaBeatmap = (ManiaBeatmap)beatmap;
|
|
||||||
|
|
||||||
int keys = 0;
|
int keys = 0;
|
||||||
|
|
||||||
foreach (var action in Actions)
|
foreach (var action in Actions)
|
||||||
{
|
keys |= 1 << (int)action;
|
||||||
switch (action)
|
|
||||||
{
|
|
||||||
case ManiaAction.Special1:
|
|
||||||
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ManiaAction.Special2:
|
|
||||||
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// the index in lazer, which doesn't include special keys.
|
|
||||||
int nonSpecialKeyIndex = action - ManiaAction.Key1;
|
|
||||||
|
|
||||||
// the index inclusive of special keys.
|
|
||||||
int overallIndex = 0;
|
|
||||||
|
|
||||||
// iterate to find the index including special keys.
|
|
||||||
for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
|
|
||||||
{
|
|
||||||
// skip over special columns.
|
|
||||||
if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
|
|
||||||
continue;
|
|
||||||
// found a non-special column to use.
|
|
||||||
if (nonSpecialKeyIndex == 0)
|
|
||||||
break;
|
|
||||||
// found a non-special column but not ours.
|
|
||||||
nonSpecialKeyIndex--;
|
|
||||||
}
|
|
||||||
|
|
||||||
keys |= 1 << overallIndex;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
|
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Find the overall index (across all stages) for a specified special key.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="maniaBeatmap">The beatmap.</param>
|
|
||||||
/// <param name="specialOffset">The special key offset (0 is S1).</param>
|
|
||||||
/// <returns>The overall index for the special column.</returns>
|
|
||||||
private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
|
|
||||||
{
|
|
||||||
if (isColumnAtIndexSpecial(maniaBeatmap, i))
|
|
||||||
{
|
|
||||||
if (specialOffset == 0)
|
|
||||||
return i;
|
|
||||||
|
|
||||||
specialOffset--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check whether the column at an overall index (across all stages) is a special column.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="beatmap">The beatmap.</param>
|
|
||||||
/// <param name="index">The overall index to check.</param>
|
|
||||||
private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
|
|
||||||
{
|
|
||||||
foreach (var stage in beatmap.Stages)
|
|
||||||
{
|
|
||||||
if (index >= stage.Columns)
|
|
||||||
{
|
|
||||||
index -= stage.Columns;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stage.IsSpecialColumn(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException("Column index is too high.", nameof(index));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
LeftKeys = leftKeys,
|
LeftKeys = leftKeys,
|
||||||
RightKeys = rightKeys,
|
RightKeys = rightKeys,
|
||||||
SpecialKey = InputKey.Space,
|
SpecialKey = InputKey.Space,
|
||||||
SpecialAction = ManiaAction.Special1,
|
}.GenerateKeyBindingsFor(variant);
|
||||||
NormalActionStart = ManiaAction.Key1,
|
|
||||||
}.GenerateKeyBindingsFor(variant, out _);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||||
|
{
|
||||||
|
public partial class ArgonManiaComboCounter : ArgonComboCounter
|
||||||
|
{
|
||||||
|
protected override bool DisplayXSymbol => false;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||||
|
|
||||||
|
private IBindable<ScrollingDirection> direction = null!;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
// the logic of flipping the position of the combo counter w.r.t. the direction does not work with "Closest" anchor,
|
||||||
|
// because it always forces the anchor to be top or bottom based on scrolling direction.
|
||||||
|
UsesFixedAnchor = true;
|
||||||
|
|
||||||
|
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||||
|
direction.BindValueChanged(_ => updateAnchor());
|
||||||
|
|
||||||
|
// two schedules are required so that updateAnchor is executed in the next frame,
|
||||||
|
// which is when the combo counter receives its Y position by the default layout in ArgonManiaSkinTransformer.
|
||||||
|
Schedule(() => Schedule(updateAnchor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAnchor()
|
||||||
|
{
|
||||||
|
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
|
||||||
|
if (Anchor.HasFlag(Anchor.y1))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||||
|
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||||
|
|
||||||
|
// change the sign of the Y coordinate in line with the scrolling direction.
|
||||||
|
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
|
||||||
|
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,8 +2,10 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -26,7 +28,34 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
|||||||
{
|
{
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
case GlobalSkinnableContainerLookup containerLookup:
|
||||||
|
// Only handle per ruleset defaults here.
|
||||||
|
if (containerLookup.Ruleset == null)
|
||||||
|
return base.GetDrawableComponent(lookup);
|
||||||
|
|
||||||
|
switch (containerLookup.Lookup)
|
||||||
|
{
|
||||||
|
case GlobalSkinnableContainers.MainHUDComponents:
|
||||||
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
|
{
|
||||||
|
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (combo != null)
|
||||||
|
{
|
||||||
|
combo.ShowLabel.Value = false;
|
||||||
|
combo.Anchor = Anchor.TopCentre;
|
||||||
|
combo.Origin = Anchor.Centre;
|
||||||
|
combo.Y = 200;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
new ArgonManiaComboCounter(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case SkinComponentLookup<HitResult> resultComponent:
|
||||||
// This should eventually be moved to a skin setting, when supported.
|
// This should eventually be moved to a skin setting, when supported.
|
||||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||||
return Drawable.Empty();
|
return Drawable.Empty();
|
||||||
|
@ -0,0 +1,194 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||||
|
{
|
||||||
|
public partial class LegacyManiaComboCounter : CompositeDrawable, ISerialisableDrawable
|
||||||
|
{
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
|
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0 };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Value shown at the current moment.
|
||||||
|
/// </summary>
|
||||||
|
public virtual int DisplayedCount
|
||||||
|
{
|
||||||
|
get => displayedCount;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
if (displayedCount.Equals(value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
displayedCountText.FadeTo(value == 0 ? 0 : 1);
|
||||||
|
displayedCountText.Text = value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
counterContainer.Size = displayedCountText.Size;
|
||||||
|
|
||||||
|
displayedCount = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int displayedCount;
|
||||||
|
|
||||||
|
private int previousValue;
|
||||||
|
|
||||||
|
private const double fade_out_duration = 100;
|
||||||
|
private const double rolling_duration = 20;
|
||||||
|
|
||||||
|
private Container counterContainer = null!;
|
||||||
|
private LegacySpriteText popOutCountText = null!;
|
||||||
|
private LegacySpriteText displayedCountText = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin, ScoreProcessor scoreProcessor)
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
counterContainer = new Container
|
||||||
|
{
|
||||||
|
AlwaysPresent = true,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
popOutCountText = new LegacySpriteText(LegacyFont.Combo)
|
||||||
|
{
|
||||||
|
Alpha = 0,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
BypassAutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red,
|
||||||
|
},
|
||||||
|
displayedCountText = new LegacySpriteText(LegacyFont.Combo)
|
||||||
|
{
|
||||||
|
Alpha = 0,
|
||||||
|
AlwaysPresent = true,
|
||||||
|
BypassAutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Current.BindTo(scoreProcessor.Combo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||||
|
|
||||||
|
private IBindable<ScrollingDirection> direction = null!;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
displayedCountText.Text = popOutCountText.Text = Current.Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true);
|
||||||
|
|
||||||
|
counterContainer.Size = displayedCountText.Size;
|
||||||
|
|
||||||
|
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||||
|
direction.BindValueChanged(_ => updateAnchor());
|
||||||
|
|
||||||
|
// two schedules are required so that updateAnchor is executed in the next frame,
|
||||||
|
// which is when the combo counter receives its Y position by the default layout in LegacyManiaSkinTransformer.
|
||||||
|
Schedule(() => Schedule(updateAnchor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAnchor()
|
||||||
|
{
|
||||||
|
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
|
||||||
|
if (Anchor.HasFlag(Anchor.y1))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||||
|
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||||
|
|
||||||
|
// change the sign of the Y coordinate in line with the scrolling direction.
|
||||||
|
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
|
||||||
|
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCount(bool rolling)
|
||||||
|
{
|
||||||
|
int prev = previousValue;
|
||||||
|
previousValue = Current.Value;
|
||||||
|
|
||||||
|
if (!IsLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!rolling)
|
||||||
|
{
|
||||||
|
FinishTransforms(false, nameof(DisplayedCount));
|
||||||
|
|
||||||
|
if (prev + 1 == Current.Value)
|
||||||
|
onCountIncrement();
|
||||||
|
else
|
||||||
|
onCountChange();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
onCountRolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCountIncrement()
|
||||||
|
{
|
||||||
|
popOutCountText.Hide();
|
||||||
|
|
||||||
|
DisplayedCount = Current.Value;
|
||||||
|
displayedCountText.ScaleTo(new Vector2(1f, 1.4f))
|
||||||
|
.ScaleTo(new Vector2(1f), 300, Easing.Out)
|
||||||
|
.FadeIn(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCountChange()
|
||||||
|
{
|
||||||
|
popOutCountText.Hide();
|
||||||
|
|
||||||
|
if (Current.Value == 0)
|
||||||
|
displayedCountText.FadeOut();
|
||||||
|
|
||||||
|
DisplayedCount = Current.Value;
|
||||||
|
|
||||||
|
displayedCountText.ScaleTo(1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCountRolling()
|
||||||
|
{
|
||||||
|
if (DisplayedCount > 0)
|
||||||
|
{
|
||||||
|
popOutCountText.Text = DisplayedCount.ToString(CultureInfo.InvariantCulture);
|
||||||
|
popOutCountText.FadeTo(0.8f).FadeOut(200)
|
||||||
|
.ScaleTo(1f).ScaleTo(4f, 200);
|
||||||
|
|
||||||
|
displayedCountText.FadeTo(0.5f, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hides displayed count if was increasing from 0 to 1 but didn't finish
|
||||||
|
if (DisplayedCount == 0 && Current.Value == 0)
|
||||||
|
displayedCountText.FadeOut(fade_out_duration);
|
||||||
|
|
||||||
|
this.TransformTo(nameof(DisplayedCount), Current.Value, getProportionalDuration(DisplayedCount, Current.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getProportionalDuration(int currentValue, int newValue)
|
||||||
|
{
|
||||||
|
double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue;
|
||||||
|
return difference * rolling_duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
@ -78,7 +80,37 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
case GlobalSkinnableContainerLookup containerLookup:
|
||||||
|
// Modifications for global components.
|
||||||
|
if (containerLookup.Ruleset == null)
|
||||||
|
return base.GetDrawableComponent(lookup);
|
||||||
|
|
||||||
|
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||||
|
if (!IsProvidingLegacyResources)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
switch (containerLookup.Lookup)
|
||||||
|
{
|
||||||
|
case GlobalSkinnableContainers.MainHUDComponents:
|
||||||
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
|
{
|
||||||
|
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (combo != null)
|
||||||
|
{
|
||||||
|
combo.Anchor = Anchor.TopCentre;
|
||||||
|
combo.Origin = Anchor.Centre;
|
||||||
|
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
new LegacyManiaComboCounter(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case SkinComponentLookup<HitResult> resultComponent:
|
||||||
return getResult(resultComponent.Component);
|
return getResult(resultComponent.Component);
|
||||||
|
|
||||||
case ManiaSkinComponentLookup maniaComponent:
|
case ManiaSkinComponentLookup maniaComponent:
|
||||||
|
@ -5,22 +5,12 @@
|
|||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.UI
|
namespace osu.Game.Rulesets.Mania.UI
|
||||||
{
|
{
|
||||||
public partial class DrawableManiaJudgement : DrawableJudgement
|
public partial class DrawableManiaJudgement : DrawableJudgement
|
||||||
{
|
{
|
||||||
public DrawableManiaJudgement(JudgementResult result, DrawableHitObject judgedObject)
|
|
||||||
: base(result, judgedObject)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public DrawableManiaJudgement()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
|
||||||
|
|
||||||
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
||||||
|
@ -66,13 +66,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
Content = new[] { new Drawable[stageDefinitions.Count] }
|
Content = new[] { new Drawable[stageDefinitions.Count] }
|
||||||
});
|
});
|
||||||
|
|
||||||
var normalColumnAction = ManiaAction.Key1;
|
var columnAction = ManiaAction.Key1;
|
||||||
var specialColumnAction = ManiaAction.Special1;
|
|
||||||
int firstColumnIndex = 0;
|
int firstColumnIndex = 0;
|
||||||
|
|
||||||
for (int i = 0; i < stageDefinitions.Count; i++)
|
for (int i = 0; i < stageDefinitions.Count; i++)
|
||||||
{
|
{
|
||||||
var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
|
var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref columnAction);
|
||||||
|
|
||||||
playfieldGrid.Content[0][i] = newStage;
|
playfieldGrid.Content[0][i] = newStage;
|
||||||
|
|
||||||
|
@ -99,12 +99,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
|
||||||
{
|
|
||||||
Show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnTouchDown(TouchDownEvent e)
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
{
|
{
|
||||||
Show();
|
Show();
|
||||||
@ -172,17 +166,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
updateButton(false);
|
updateButton(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
|
||||||
{
|
|
||||||
updateButton(true);
|
|
||||||
return false; // handled by parent container to show overlay.
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
|
||||||
{
|
|
||||||
updateButton(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateButton(bool press)
|
private void updateButton(bool press)
|
||||||
{
|
{
|
||||||
if (press == isPressed)
|
if (press == isPressed)
|
||||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
private ISkinSource currentSkin = null!;
|
private ISkinSource currentSkin = null!;
|
||||||
|
|
||||||
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
|
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction columnStartAction)
|
||||||
{
|
{
|
||||||
this.firstColumnIndex = firstColumnIndex;
|
this.firstColumnIndex = firstColumnIndex;
|
||||||
Definition = definition;
|
Definition = definition;
|
||||||
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Width = 1,
|
Width = 1,
|
||||||
Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ }
|
Action = { Value = columnStartAction++ }
|
||||||
};
|
};
|
||||||
|
|
||||||
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
|
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
|
||||||
|
@ -26,37 +26,30 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
public InputKey SpecialKey;
|
public InputKey SpecialKey;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
|
/// The <see cref="ManiaAction"/> at which the columns should begin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ManiaAction NormalActionStart;
|
public ManiaAction ActionStart;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="ManiaAction"/> for the special column.
|
|
||||||
/// </summary>
|
|
||||||
public ManiaAction SpecialAction;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
|
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="columns">The number of columns that need to be bound.</param>
|
/// <param name="columns">The number of columns that need to be bound.</param>
|
||||||
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
|
|
||||||
/// <returns>The keybindings.</returns>
|
/// <returns>The keybindings.</returns>
|
||||||
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
|
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns)
|
||||||
{
|
{
|
||||||
ManiaAction currentNormalAction = NormalActionStart;
|
ManiaAction currentAction = ActionStart;
|
||||||
|
|
||||||
var bindings = new List<KeyBinding>();
|
var bindings = new List<KeyBinding>();
|
||||||
|
|
||||||
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
|
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
|
||||||
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
|
bindings.Add(new KeyBinding(LeftKeys[i], currentAction++));
|
||||||
|
|
||||||
if (columns % 2 == 1)
|
if (columns % 2 == 1)
|
||||||
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
|
bindings.Add(new KeyBinding(SpecialKey, currentAction++));
|
||||||
|
|
||||||
for (int i = 0; i < columns / 2; i++)
|
for (int i = 0; i < columns / 2; i++)
|
||||||
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
|
bindings.Add(new KeyBinding(RightKeys[i], currentAction++));
|
||||||
|
|
||||||
nextNormalAction = currentNormalAction;
|
|
||||||
return bindings;
|
return bindings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
});
|
});
|
||||||
|
|
||||||
moveMouseToHitObject(1);
|
moveMouseToHitObject(1);
|
||||||
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true);
|
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
|
||||||
|
|
||||||
mergeSelection();
|
mergeSelection();
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
});
|
});
|
||||||
|
|
||||||
moveMouseToHitObject(1);
|
moveMouseToHitObject(1);
|
||||||
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems?.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
|
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
|
||||||
mergeSelection();
|
mergeSelection();
|
||||||
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
|
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
|
||||||
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
|
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
|
||||||
@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
});
|
});
|
||||||
|
|
||||||
moveMouseToHitObject(1);
|
moveMouseToHitObject(1);
|
||||||
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true);
|
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
|
||||||
|
|
||||||
mergeSelection();
|
mergeSelection();
|
||||||
|
|
||||||
|
@ -24,24 +24,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestGridToggles()
|
public void TestGridToggles()
|
||||||
{
|
{
|
||||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
|
|
||||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
gridActive<RectangularPositionSnapGrid>(false);
|
gridActive<RectangularPositionSnapGrid>(false);
|
||||||
|
|
||||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
|
|
||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
gridActive<RectangularPositionSnapGrid>(true);
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
|
||||||
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
|
AddStep("disable distance snap grid", () => InputManager.Key(Key.Y));
|
||||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
gridActive<RectangularPositionSnapGrid>(true);
|
gridActive<RectangularPositionSnapGrid>(true);
|
||||||
|
|
||||||
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
|
AddStep("disable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
gridActive<RectangularPositionSnapGrid>(false);
|
gridActive<RectangularPositionSnapGrid>(false);
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
|
|
||||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
|
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
|
||||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
{
|
{
|
||||||
double distanceSnap = double.PositiveInfinity;
|
double distanceSnap = double.PositiveInfinity;
|
||||||
|
|
||||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||||
|
|
||||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||||
@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestGridSizeToggling()
|
public void TestGridSizeToggling()
|
||||||
{
|
{
|
||||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||||
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||||
gridSizeIs(4);
|
gridSizeIs(4);
|
||||||
|
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
|
{
|
||||||
|
public partial class TestSceneSliderChangeStates : TestSceneOsuEditor
|
||||||
|
{
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
|
[TestCase(SplineType.Catmull)]
|
||||||
|
[TestCase(SplineType.BSpline)]
|
||||||
|
[TestCase(SplineType.Linear)]
|
||||||
|
[TestCase(SplineType.PerfectCurve)]
|
||||||
|
public void TestSliderRetainsCurveTypes(SplineType splineType)
|
||||||
|
{
|
||||||
|
Slider? slider = null;
|
||||||
|
PathType pathType = new PathType(splineType);
|
||||||
|
|
||||||
|
AddStep("add slider", () => EditorBeatmap.Add(slider = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 500,
|
||||||
|
Path = new SliderPath(new[]
|
||||||
|
{
|
||||||
|
new PathControlPoint(Vector2.Zero, pathType),
|
||||||
|
new PathControlPoint(new Vector2(200, 0), pathType),
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType));
|
||||||
|
AddStep("remove object", () => EditorBeatmap.Remove(slider));
|
||||||
|
AddAssert("slider removed", () => EditorBeatmap.HitObjects.Count == 0);
|
||||||
|
addUndoSteps();
|
||||||
|
AddAssert("slider not removed", () => EditorBeatmap.HitObjects.Count == 1);
|
||||||
|
AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
|
||||||
|
}
|
||||||
|
}
|
@ -299,6 +299,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
});
|
});
|
||||||
assertControlPointTypeDuringPlacement(0, PathType.BSpline(4));
|
assertControlPointTypeDuringPlacement(0, PathType.BSpline(4));
|
||||||
|
|
||||||
|
AddStep("press alt-2", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.AltLeft);
|
||||||
|
InputManager.Key(Key.Number2);
|
||||||
|
InputManager.ReleaseKey(Key.AltLeft);
|
||||||
|
});
|
||||||
|
assertControlPointTypeDuringPlacement(0, PathType.BEZIER);
|
||||||
|
|
||||||
AddStep("start new segment via S", () => InputManager.Key(Key.S));
|
AddStep("start new segment via S", () => InputManager.Key(Key.S));
|
||||||
assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
|
assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
|
||||||
|
|
||||||
@ -309,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
addClickStep(MouseButton.Right);
|
addClickStep(MouseButton.Right);
|
||||||
|
|
||||||
assertPlaced(true);
|
assertPlaced(true);
|
||||||
assertFinalControlPointType(0, PathType.BSpline(4));
|
assertFinalControlPointType(0, PathType.BEZIER);
|
||||||
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
|
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +163,44 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
checkControlPointSelected(1, false);
|
checkControlPointSelected(1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAdjustLength()
|
||||||
|
{
|
||||||
|
AddStep("move mouse to drag marker", () =>
|
||||||
|
{
|
||||||
|
Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0);
|
||||||
|
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
|
||||||
|
});
|
||||||
|
AddStep("start drag", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
AddStep("move mouse to control point 1", () =>
|
||||||
|
{
|
||||||
|
Vector2 position = slider.Position + slider.Path.ControlPoints[1].Position + new Vector2(60, 0);
|
||||||
|
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
|
||||||
|
});
|
||||||
|
AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
AddAssert("expected distance halved",
|
||||||
|
() => Precision.AlmostEquals(slider.Path.Distance, 172.2, 0.1));
|
||||||
|
|
||||||
|
AddStep("move mouse to drag marker", () =>
|
||||||
|
{
|
||||||
|
Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0);
|
||||||
|
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
|
||||||
|
});
|
||||||
|
AddStep("start drag", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
AddStep("move mouse beyond last control point", () =>
|
||||||
|
{
|
||||||
|
Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(100, 0);
|
||||||
|
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
|
||||||
|
});
|
||||||
|
AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
AddAssert("expected distance is calculated distance",
|
||||||
|
() => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1));
|
||||||
|
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddAssert("expected distance is unchanged",
|
||||||
|
() => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
private void moveHitObject()
|
private void moveHitObject()
|
||||||
{
|
{
|
||||||
AddStep("move hitobject", () =>
|
AddStep("move hitobject", () =>
|
||||||
|
@ -88,6 +88,21 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
|
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClickExpand()
|
||||||
|
{
|
||||||
|
createTest(() => new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Scale = new Vector2(10),
|
||||||
|
Child = new CursorTrail(),
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("expand", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = new Vector2(3));
|
||||||
|
AddWaitStep("let the cursor trail draw a bit", 5);
|
||||||
|
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
||||||
|
}
|
||||||
|
|
||||||
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
|
133
osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs
Normal file
133
osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public partial class TestSceneOsuAnalysisContainer : OsuTestScene
|
||||||
|
{
|
||||||
|
private TestReplayAnalysisOverlay analysisContainer = null!;
|
||||||
|
private ReplayAnalysisSettings settings = null!;
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo);
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("create analysis container", () =>
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuPlayfieldAdjustmentContainer
|
||||||
|
{
|
||||||
|
Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()),
|
||||||
|
},
|
||||||
|
settings = new ReplayAnalysisSettings(config),
|
||||||
|
};
|
||||||
|
|
||||||
|
settings.ShowClickMarkers.Value = false;
|
||||||
|
settings.ShowAimMarkers.Value = false;
|
||||||
|
settings.ShowCursorPath.Value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEverythingOn()
|
||||||
|
{
|
||||||
|
AddStep("enable everything", () =>
|
||||||
|
{
|
||||||
|
settings.ShowClickMarkers.Value = true;
|
||||||
|
settings.ShowAimMarkers.Value = true;
|
||||||
|
settings.ShowCursorPath.Value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitMarkers()
|
||||||
|
{
|
||||||
|
AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true);
|
||||||
|
AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible);
|
||||||
|
AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false);
|
||||||
|
AddUntilStep("hit markers not visible", () => !analysisContainer.HitMarkersVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAimMarker()
|
||||||
|
{
|
||||||
|
AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true);
|
||||||
|
AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible);
|
||||||
|
AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false);
|
||||||
|
AddUntilStep("aim markers not visible", () => !analysisContainer.AimMarkersVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAimLines()
|
||||||
|
{
|
||||||
|
AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true);
|
||||||
|
AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible);
|
||||||
|
AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false);
|
||||||
|
AddUntilStep("aim lines not visible", () => !analysisContainer.AimLinesVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Replay fabricateReplay()
|
||||||
|
{
|
||||||
|
var frames = new List<ReplayFrame>();
|
||||||
|
var random = new Random();
|
||||||
|
int posX = 250;
|
||||||
|
int posY = 250;
|
||||||
|
|
||||||
|
var actions = new HashSet<OsuAction>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 1000; i++)
|
||||||
|
{
|
||||||
|
posX = Math.Clamp(posX + random.Next(-20, 21), -100, 600);
|
||||||
|
posY = Math.Clamp(posY + random.Next(-20, 21), -100, 600);
|
||||||
|
|
||||||
|
if (random.NextDouble() > (actions.Count == 0 ? 0.9 : 0.95))
|
||||||
|
{
|
||||||
|
actions.Add(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton);
|
||||||
|
}
|
||||||
|
else if (random.NextDouble() > 0.7)
|
||||||
|
{
|
||||||
|
actions.Remove(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
frames.Add(new OsuReplayFrame
|
||||||
|
{
|
||||||
|
Time = Time.Current + i * 15,
|
||||||
|
Position = new Vector2(posX, posY),
|
||||||
|
Actions = actions.ToList(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Replay { Frames = frames };
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class TestReplayAnalysisOverlay : ReplayAnalysisOverlay
|
||||||
|
{
|
||||||
|
public TestReplayAnalysisOverlay(Replay replay)
|
||||||
|
: base(replay)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HitMarkersVisible => ClickMarkers?.Alpha > 0 && ClickMarkers.Entries.Any();
|
||||||
|
public bool AimMarkersVisible => FrameMarkers?.Alpha > 0 && FrameMarkers.Entries.Any();
|
||||||
|
public bool AimLinesVisible => CursorPath?.Alpha > 0 && CursorPath.Vertices.Count > 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Configuration;
|
using osu.Game.Rulesets.Configuration;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -11,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
|||||||
{
|
{
|
||||||
public class OsuRulesetConfigManager : RulesetConfigManager<OsuRulesetSetting>
|
public class OsuRulesetConfigManager : RulesetConfigManager<OsuRulesetSetting>
|
||||||
{
|
{
|
||||||
public OsuRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
|
public OsuRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
|
||||||
: base(settings, ruleset, variant)
|
: base(settings, ruleset, variant)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -24,6 +22,12 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
|||||||
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
|
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
|
||||||
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
|
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
|
||||||
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
|
SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
|
||||||
|
|
||||||
|
SetDefault(OsuRulesetSetting.ReplayClickMarkersEnabled, false);
|
||||||
|
SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false);
|
||||||
|
SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false);
|
||||||
|
SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false);
|
||||||
|
SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 800);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,5 +38,12 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
|||||||
ShowCursorTrail,
|
ShowCursorTrail,
|
||||||
ShowCursorRipples,
|
ShowCursorRipples,
|
||||||
PlayfieldBorderStyle,
|
PlayfieldBorderStyle,
|
||||||
|
|
||||||
|
// Replay
|
||||||
|
ReplayClickMarkersEnabled,
|
||||||
|
ReplayFrameMarkersEnabled,
|
||||||
|
ReplayCursorPathEnabled,
|
||||||
|
ReplayCursorHideEnabled,
|
||||||
|
ReplayAnalysisDisplayLength,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
{
|
{
|
||||||
public static class SpeedEvaluator
|
public static class SpeedEvaluator
|
||||||
{
|
{
|
||||||
private const double single_spacing_threshold = 125;
|
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
|
||||||
private const double min_speed_bonus = 75; // ~200BPM
|
private const double min_speed_bonus = 75; // ~200BPM
|
||||||
private const double speed_balancing_factor = 40;
|
private const double speed_balancing_factor = 40;
|
||||||
|
|
||||||
@ -50,16 +50,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
|||||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||||
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
|
||||||
|
|
||||||
// derive speedBonus for calculation
|
// speedBonus will be 1.0 for BPM < 200
|
||||||
double speedBonus = 1.0;
|
double speedBonus = 1.0;
|
||||||
|
|
||||||
|
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
||||||
if (strainTime < min_speed_bonus)
|
if (strainTime < min_speed_bonus)
|
||||||
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||||
|
|
||||||
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||||
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
|
double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
|
||||||
|
|
||||||
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) * doubletapness / strainTime;
|
// Cap distance at single_spacing_threshold
|
||||||
|
distance = Math.Min(distance, single_spacing_threshold);
|
||||||
|
|
||||||
|
// Max distance bonus is 2 at single_spacing_threshold
|
||||||
|
double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5);
|
||||||
|
|
||||||
|
// Base difficulty with all bonuses
|
||||||
|
double difficulty = speedBonus * distanceBonus * 1000 / strainTime;
|
||||||
|
|
||||||
|
// Apply penalty if there's doubletappable doubles
|
||||||
|
return difficulty * doubletapness;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
flashlightRating *= 0.7;
|
flashlightRating *= 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
|
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
||||||
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
|
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
||||||
double baseFlashlightPerformance = 0.0;
|
double baseFlashlightPerformance = 0.0;
|
||||||
|
|
||||||
if (mods.Any(h => h is OsuModFlashlight))
|
if (mods.Any(h => h is OsuModFlashlight))
|
||||||
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0;
|
baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
|
||||||
|
|
||||||
double basePerformance =
|
double basePerformance =
|
||||||
Math.Pow(
|
Math.Pow(
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||||
{
|
{
|
||||||
double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
|
||||||
|
|
||||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (score.Mods.Any(h => h is OsuModRelax))
|
if (score.Mods.Any(h => h is OsuModRelax))
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
|
||||||
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||||
|
|
||||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
@ -226,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (!score.Mods.Any(h => h is OsuModFlashlight))
|
if (!score.Mods.Any(h => h is OsuModFlashlight))
|
||||||
return 0.0;
|
return 0.0;
|
||||||
|
|
||||||
double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
|
double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty);
|
||||||
|
|
||||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||||
if (effectiveMissCount > 0)
|
if (effectiveMissCount > 0)
|
||||||
|
@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
|
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
|
||||||
|
|
||||||
|
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,5 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
return difficulty;
|
return difficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : OsuStrainSkill
|
public class Speed : OsuStrainSkill
|
||||||
{
|
{
|
||||||
private double skillMultiplier => 1430;
|
private double skillMultiplier => 1.430;
|
||||||
private double strainDecayBase => 0.3;
|
private double strainDecayBase => 0.3;
|
||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public readonly PathControlPoint ControlPoint;
|
public readonly PathControlPoint ControlPoint;
|
||||||
|
|
||||||
private readonly T hitObject;
|
private readonly T hitObject;
|
||||||
private readonly Circle circle;
|
private readonly FastCircle circle;
|
||||||
private readonly Drawable markerRing;
|
private readonly Drawable markerRing;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
|
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
circle = new Circle
|
circle = new FastCircle
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -309,8 +309,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (!e.AltPressed)
|
if (!e.AltPressed)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// If no pieces are selected, we can't change the path type.
|
||||||
|
if (Pieces.All(p => !p.IsSelected.Value))
|
||||||
|
return false;
|
||||||
|
|
||||||
var type = path_types[e.Key - Key.Number1];
|
var type = path_types[e.Key - Key.Number1];
|
||||||
|
|
||||||
|
// The first control point can never be inherit type
|
||||||
if (Pieces[0].IsSelected.Value && type == null)
|
if (Pieces[0].IsSelected.Value && type == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
@ -9,11 +12,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
public partial class SliderCircleOverlay : CompositeDrawable
|
public partial class SliderCircleOverlay : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
public SliderEndDragMarker? EndDragMarker { get; }
|
||||||
|
|
||||||
|
public RectangleF VisibleQuad
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var result = CirclePiece.ScreenSpaceDrawQuad.AABBFloat;
|
||||||
|
|
||||||
|
if (endDragMarkerContainer == null) return result;
|
||||||
|
|
||||||
|
var size = result.Size * 1.4f;
|
||||||
|
var location = result.TopLeft - result.Size * 0.2f;
|
||||||
|
return new RectangleF(location, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly HitCirclePiece CirclePiece;
|
protected readonly HitCirclePiece CirclePiece;
|
||||||
|
|
||||||
private readonly Slider slider;
|
private readonly Slider slider;
|
||||||
private readonly SliderPosition position;
|
private readonly SliderPosition position;
|
||||||
private readonly HitCircleOverlapMarker? marker;
|
private readonly HitCircleOverlapMarker? marker;
|
||||||
|
private readonly Container? endDragMarkerContainer;
|
||||||
|
|
||||||
public SliderCircleOverlay(Slider slider, SliderPosition position)
|
public SliderCircleOverlay(Slider slider, SliderPosition position)
|
||||||
{
|
{
|
||||||
@ -24,26 +44,49 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
AddInternal(marker = new HitCircleOverlapMarker());
|
AddInternal(marker = new HitCircleOverlapMarker());
|
||||||
|
|
||||||
AddInternal(CirclePiece = new HitCirclePiece());
|
AddInternal(CirclePiece = new HitCirclePiece());
|
||||||
|
|
||||||
|
if (position == SliderPosition.End)
|
||||||
|
{
|
||||||
|
AddInternal(endDragMarkerContainer = new Container
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Padding = new MarginPadding(-2.5f),
|
||||||
|
Child = EndDragMarker = new SliderEndDragMarker()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
|
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle :
|
||||||
|
slider.RepeatCount % 2 == 0 ? slider.TailCircle : slider.LastRepeat!;
|
||||||
|
|
||||||
CirclePiece.UpdateFrom(circle);
|
CirclePiece.UpdateFrom(circle);
|
||||||
marker?.UpdateFrom(circle);
|
marker?.UpdateFrom(circle);
|
||||||
|
|
||||||
|
if (endDragMarkerContainer != null)
|
||||||
|
{
|
||||||
|
endDragMarkerContainer.Position = circle.Position;
|
||||||
|
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
|
||||||
|
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
|
||||||
|
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Hide()
|
public override void Hide()
|
||||||
{
|
{
|
||||||
CirclePiece.Hide();
|
CirclePiece.Hide();
|
||||||
|
endDragMarkerContainer?.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Show()
|
public override void Show()
|
||||||
{
|
{
|
||||||
CirclePiece.Show();
|
CirclePiece.Show();
|
||||||
|
endDragMarkerContainer?.Show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Lines;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||||
|
{
|
||||||
|
public partial class SliderEndDragMarker : SmoothPath
|
||||||
|
{
|
||||||
|
public Action<DragStartEvent>? StartDrag { get; set; }
|
||||||
|
public Action<DragEvent>? Drag { get; set; }
|
||||||
|
public Action? EndDrag { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
var path = PathApproximator.CircularArcToPiecewiseLinear([
|
||||||
|
new Vector2(0, OsuHitObject.OBJECT_RADIUS),
|
||||||
|
new Vector2(OsuHitObject.OBJECT_RADIUS, 0),
|
||||||
|
new Vector2(0, -OsuHitObject.OBJECT_RADIUS)
|
||||||
|
]);
|
||||||
|
|
||||||
|
Anchor = Anchor.CentreLeft;
|
||||||
|
Origin = Anchor.CentreLeft;
|
||||||
|
PathRadius = 5;
|
||||||
|
Vertices = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
updateState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
updateState();
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
|
{
|
||||||
|
updateState();
|
||||||
|
StartDrag?.Invoke(e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDrag(DragEvent e)
|
||||||
|
{
|
||||||
|
updateState();
|
||||||
|
base.OnDrag(e);
|
||||||
|
Drag?.Invoke(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
{
|
||||||
|
updateState();
|
||||||
|
EndDrag?.Invoke();
|
||||||
|
base.OnDragEnd(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,9 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
@ -29,30 +25,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
public new Slider HitObject => (Slider)base.HitObject;
|
public new Slider HitObject => (Slider)base.HitObject;
|
||||||
|
|
||||||
private SliderBodyPiece bodyPiece;
|
private SliderBodyPiece bodyPiece = null!;
|
||||||
private HitCirclePiece headCirclePiece;
|
private HitCirclePiece headCirclePiece = null!;
|
||||||
private HitCirclePiece tailCirclePiece;
|
private HitCirclePiece tailCirclePiece = null!;
|
||||||
private PathControlPointVisualiser<Slider> controlPointVisualiser;
|
private PathControlPointVisualiser<Slider> controlPointVisualiser = null!;
|
||||||
|
|
||||||
private InputManager inputManager;
|
private InputManager inputManager = null!;
|
||||||
|
|
||||||
|
private PathControlPoint? cursor;
|
||||||
|
|
||||||
private SliderPlacementState state;
|
private SliderPlacementState state;
|
||||||
private PathControlPoint segmentStart;
|
private PathControlPoint segmentStart;
|
||||||
private PathControlPoint cursor;
|
|
||||||
private int currentSegmentLength;
|
private int currentSegmentLength;
|
||||||
private bool usingCustomSegmentType;
|
private bool usingCustomSegmentType;
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
[CanBeNull]
|
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||||
private IPositionSnapProvider positionSnapProvider { get; set; }
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
[CanBeNull]
|
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
[CanBeNull]
|
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
|
||||||
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
|
|
||||||
|
|
||||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||||
|
|
||||||
@ -84,7 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
inputManager = GetContainingInputManager();
|
|
||||||
|
inputManager = GetContainingInputManager()!;
|
||||||
|
|
||||||
if (freehandToolboxGroup != null)
|
if (freehandToolboxGroup != null)
|
||||||
{
|
{
|
||||||
@ -108,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private EditorBeatmap editorBeatmap { get; set; }
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
public override void UpdateTimeAndPosition(SnapResult result)
|
public override void UpdateTimeAndPosition(SnapResult result)
|
||||||
{
|
{
|
||||||
@ -151,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
case SliderPlacementState.ControlPoints:
|
case SliderPlacementState.ControlPoints:
|
||||||
if (canPlaceNewControlPoint(out var lastPoint))
|
if (canPlaceNewControlPoint(out var lastPoint))
|
||||||
placeNewControlPoint();
|
placeNewControlPoint();
|
||||||
else
|
else if (lastPoint != null)
|
||||||
beginNewSegment(lastPoint);
|
beginNewSegment(lastPoint);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -162,9 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private void beginNewSegment(PathControlPoint lastPoint)
|
private void beginNewSegment(PathControlPoint lastPoint)
|
||||||
{
|
{
|
||||||
// Transform the last point into a new segment.
|
|
||||||
Debug.Assert(lastPoint != null);
|
|
||||||
|
|
||||||
segmentStart = lastPoint;
|
segmentStart = lastPoint;
|
||||||
segmentStart.Type = PathType.LINEAR;
|
segmentStart.Type = PathType.LINEAR;
|
||||||
|
|
||||||
@ -359,8 +352,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the cursor position.
|
// Update the cursor position.
|
||||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
|
cursor.Position = getCursorPosition();
|
||||||
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
|
||||||
}
|
}
|
||||||
else if (cursor != null)
|
else if (cursor != null)
|
||||||
{
|
{
|
||||||
@ -374,19 +366,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Vector2 getCursorPosition()
|
||||||
|
{
|
||||||
|
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
|
||||||
|
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether a new control point can be placed at the current mouse position.
|
/// Whether a new control point can be placed at the current mouse position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="lastPoint">The last-placed control point. May be null, but is not null if <c>false</c> is returned.</param>
|
/// <param name="lastPoint">The last-placed control point. May be null, but is not null if <c>false</c> is returned.</param>
|
||||||
/// <returns>Whether a new control point can be placed at the current position.</returns>
|
/// <returns>Whether a new control point can be placed at the current position.</returns>
|
||||||
private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint)
|
private bool canPlaceNewControlPoint(out PathControlPoint? lastPoint)
|
||||||
{
|
{
|
||||||
// We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point.
|
// We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point.
|
||||||
var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor);
|
var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor);
|
||||||
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
|
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
|
||||||
|
|
||||||
lastPoint = last;
|
lastPoint = last;
|
||||||
return lastPiece.IsHovered != true;
|
// We may only place a new control point if the cursor is not overlapping with the last control point.
|
||||||
|
// If snapping is enabled, the cursor may not hover the last piece while still placing the control point at the same position.
|
||||||
|
return !lastPiece.IsHovered && (last is null || Vector2.DistanceSquared(last.Position, getCursorPosition()) > 1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void placeNewControlPoint()
|
private void placeNewControlPoint()
|
||||||
@ -429,7 +429,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
// Replace this segment with a circular arc if it is a reasonable substitute.
|
// Replace this segment with a circular arc if it is a reasonable substitute.
|
||||||
var circleArcSegment = tryCircleArc(segment);
|
var circleArcSegment = tryCircleArc(segment);
|
||||||
|
|
||||||
if (circleArcSegment is not null)
|
if (circleArcSegment != null)
|
||||||
{
|
{
|
||||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
|
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
|
||||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
|
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
|
||||||
@ -446,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2[] tryCircleArc(List<Vector2> segment)
|
private Vector2[]? tryCircleArc(List<Vector2> segment)
|
||||||
{
|
{
|
||||||
if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null;
|
if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null;
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
using System;
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
@ -33,27 +33,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
|
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
|
||||||
|
|
||||||
protected SliderBodyPiece BodyPiece { get; private set; }
|
protected SliderBodyPiece BodyPiece { get; private set; } = null!;
|
||||||
protected SliderCircleOverlay HeadOverlay { get; private set; }
|
protected SliderCircleOverlay HeadOverlay { get; private set; } = null!;
|
||||||
protected SliderCircleOverlay TailOverlay { get; private set; }
|
protected SliderCircleOverlay TailOverlay { get; private set; } = null!;
|
||||||
|
|
||||||
[CanBeNull]
|
protected PathControlPointVisualiser<Slider>? ControlPointVisualiser { get; private set; }
|
||||||
protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
private IPlacementHandler placementHandler { get; set; }
|
private IPlacementHandler? placementHandler { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
private EditorBeatmap editorBeatmap { get; set; }
|
private EditorBeatmap? editorBeatmap { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
private IEditorChangeHandler changeHandler { get; set; }
|
private IEditorChangeHandler? changeHandler { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved]
|
||||||
private BindableBeatDivisor beatDivisor { get; set; }
|
private BindableBeatDivisor? beatDivisor { get; set; }
|
||||||
|
|
||||||
|
private PathControlPoint? placementControlPoint;
|
||||||
|
|
||||||
public override Quad SelectionQuad
|
public override Quad SelectionQuad
|
||||||
{
|
{
|
||||||
@ -61,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
|
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
|
||||||
|
|
||||||
|
result = RectangleF.Union(result, HeadOverlay.VisibleQuad);
|
||||||
|
result = RectangleF.Union(result, TailOverlay.VisibleQuad);
|
||||||
|
|
||||||
if (ControlPointVisualiser != null)
|
if (ControlPointVisualiser != null)
|
||||||
{
|
{
|
||||||
foreach (var piece in ControlPointVisualiser.Pieces)
|
foreach (var piece in ControlPointVisualiser.Pieces)
|
||||||
@ -76,6 +80,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
|
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
|
||||||
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
|
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
|
||||||
|
|
||||||
|
// Cached slider path which ignored the expected distance value.
|
||||||
|
private readonly Cached<SliderPath> fullPathCache = new Cached<SliderPath>();
|
||||||
|
|
||||||
|
private Vector2 lastRightClickPosition;
|
||||||
|
|
||||||
public SliderSelectionBlueprint(Slider slider)
|
public SliderSelectionBlueprint(Slider slider)
|
||||||
: base(slider)
|
: base(slider)
|
||||||
{
|
{
|
||||||
@ -91,6 +100,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
|
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// tail will always have a non-null end drag marker.
|
||||||
|
Debug.Assert(TailOverlay.EndDragMarker != null);
|
||||||
|
|
||||||
|
TailOverlay.EndDragMarker.StartDrag += startAdjustingLength;
|
||||||
|
TailOverlay.EndDragMarker.Drag += adjustLength;
|
||||||
|
TailOverlay.EndDragMarker.EndDrag += endAdjustLength;
|
||||||
|
|
||||||
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
|
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
controlPoints.BindTo(HitObject.Path.ControlPoints);
|
controlPoints.BindTo(HitObject.Path.ControlPoints);
|
||||||
|
controlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate();
|
||||||
|
|
||||||
pathVersion.BindTo(HitObject.Path.Version);
|
pathVersion.BindTo(HitObject.Path.Version);
|
||||||
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
|
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
|
||||||
@ -123,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
hoveredControlPoint.IsSelected.Value = true;
|
hoveredControlPoint.IsSelected.Value = true;
|
||||||
ControlPointVisualiser.DeleteSelected();
|
ControlPointVisualiser?.DeleteSelected();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
updateVisualDefinition();
|
updateVisualDefinition();
|
||||||
|
|
||||||
return base.OnHover(e);
|
return base.OnHover(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,17 +202,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector2 rightClickPosition;
|
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
{
|
{
|
||||||
switch (e.Button)
|
switch (e.Button)
|
||||||
{
|
{
|
||||||
case MouseButton.Right:
|
case MouseButton.Right:
|
||||||
rightClickPosition = e.MouseDownPosition;
|
lastRightClickPosition = e.MouseDownPosition;
|
||||||
return false; // Allow right click to be handled by context menu
|
return false; // Allow right click to be handled by context menu
|
||||||
|
|
||||||
case MouseButton.Left:
|
case MouseButton.Left:
|
||||||
|
|
||||||
// If there's more than two objects selected, ctrl+click should deselect
|
// If there's more than two objects selected, ctrl+click should deselect
|
||||||
if (e.ControlPressed && IsSelected && selectedObjects.Count < 2)
|
if (e.ControlPressed && IsSelected && selectedObjects.Count < 2)
|
||||||
{
|
{
|
||||||
@ -212,8 +227,134 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
[CanBeNull]
|
#region Length Adjustment (independent of path nodes)
|
||||||
private PathControlPoint placementControlPoint;
|
|
||||||
|
private Vector2 lengthAdjustMouseOffset;
|
||||||
|
private double oldDuration;
|
||||||
|
private double oldVelocityMultiplier;
|
||||||
|
private double desiredDistance;
|
||||||
|
private bool isAdjustingLength;
|
||||||
|
private bool adjustVelocityMomentary;
|
||||||
|
|
||||||
|
private void startAdjustingLength(DragStartEvent e)
|
||||||
|
{
|
||||||
|
isAdjustingLength = true;
|
||||||
|
adjustVelocityMomentary = e.ShiftPressed;
|
||||||
|
lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1);
|
||||||
|
oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier;
|
||||||
|
oldVelocityMultiplier = HitObject.SliderVelocityMultiplier;
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endAdjustLength()
|
||||||
|
{
|
||||||
|
trimExcessControlPoints(HitObject.Path);
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
isAdjustingLength = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustLength(MouseEvent e) => adjustLength(findClosestPathDistance(e), e.ShiftPressed);
|
||||||
|
|
||||||
|
private void adjustLength(double proposedDistance, bool adjustVelocity)
|
||||||
|
{
|
||||||
|
desiredDistance = proposedDistance;
|
||||||
|
double proposedVelocity = oldVelocityMultiplier;
|
||||||
|
|
||||||
|
if (adjustVelocity)
|
||||||
|
{
|
||||||
|
proposedVelocity = proposedDistance / oldDuration;
|
||||||
|
proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
||||||
|
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||||
|
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance;
|
||||||
|
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
|
||||||
|
return;
|
||||||
|
|
||||||
|
HitObject.SliderVelocityMultiplier = proposedVelocity;
|
||||||
|
HitObject.Path.ExpectedDistance.Value = proposedDistance;
|
||||||
|
editorBeatmap?.Update(HitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trims control points from the end of the slider path which are not required to reach the expected end of the slider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sliderPath">The slider path to trim control points of.</param>
|
||||||
|
private void trimExcessControlPoints(SliderPath sliderPath)
|
||||||
|
{
|
||||||
|
if (!sliderPath.ExpectedDistance.Value.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
|
||||||
|
int segmentIndex = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++)
|
||||||
|
{
|
||||||
|
if (!sliderPath.ControlPoints[i].Type.HasValue) continue;
|
||||||
|
|
||||||
|
if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3))
|
||||||
|
{
|
||||||
|
sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1);
|
||||||
|
sliderPath.ControlPoints[^1].Type = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the expected distance value for which the slider end is closest to the mouse position.
|
||||||
|
/// </summary>
|
||||||
|
private double findClosestPathDistance(MouseEvent e)
|
||||||
|
{
|
||||||
|
const double step1 = 10;
|
||||||
|
const double step2 = 0.1;
|
||||||
|
const double longer_distance_bias = 0.01;
|
||||||
|
|
||||||
|
var desiredPosition = ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position - lengthAdjustMouseOffset;
|
||||||
|
|
||||||
|
if (!fullPathCache.IsValid)
|
||||||
|
fullPathCache.Value = new SliderPath(HitObject.Path.ControlPoints.ToArray());
|
||||||
|
|
||||||
|
// Do a linear search to find the closest point on the path to the mouse position.
|
||||||
|
double bestValue = 0;
|
||||||
|
double minDistance = double.MaxValue;
|
||||||
|
|
||||||
|
for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1)
|
||||||
|
{
|
||||||
|
double t = d / fullPathCache.Value.CalculatedDistance;
|
||||||
|
double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias;
|
||||||
|
|
||||||
|
if (dist >= minDistance) continue;
|
||||||
|
|
||||||
|
minDistance = dist;
|
||||||
|
bestValue = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do another linear search to fine-tune the result.
|
||||||
|
double maxValue = Math.Min(bestValue + step1, fullPathCache.Value.CalculatedDistance);
|
||||||
|
|
||||||
|
for (double d = bestValue - step1; d <= maxValue; d += step2)
|
||||||
|
{
|
||||||
|
double t = d / fullPathCache.Value.CalculatedDistance;
|
||||||
|
double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias;
|
||||||
|
|
||||||
|
if (dist >= minDistance) continue;
|
||||||
|
|
||||||
|
minDistance = dist;
|
||||||
|
bestValue = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e)
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
{
|
{
|
||||||
@ -255,9 +396,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAdjustingLength && e.ShiftPressed != adjustVelocityMomentary)
|
||||||
|
{
|
||||||
|
adjustVelocityMomentary = e.ShiftPressed;
|
||||||
|
adjustLength(desiredDistance, adjustVelocityMomentary);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyUp(KeyUpEvent e)
|
||||||
|
{
|
||||||
|
if (!IsSelected || !isAdjustingLength || e.ShiftPressed == adjustVelocityMomentary) return;
|
||||||
|
|
||||||
|
adjustVelocityMomentary = e.ShiftPressed;
|
||||||
|
adjustLength(desiredDistance, adjustVelocityMomentary);
|
||||||
|
}
|
||||||
|
|
||||||
private PathControlPoint addControlPoint(Vector2 position)
|
private PathControlPoint addControlPoint(Vector2 position)
|
||||||
{
|
{
|
||||||
position -= HitObject.Position;
|
position -= HitObject.Position;
|
||||||
@ -326,6 +482,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
|
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
|
||||||
{
|
{
|
||||||
|
if (editorBeatmap == null)
|
||||||
|
return;
|
||||||
|
|
||||||
// Arbitrary gap in milliseconds to put between split slider pieces
|
// Arbitrary gap in milliseconds to put between split slider pieces
|
||||||
const double split_gap = 100;
|
const double split_gap = 100;
|
||||||
|
|
||||||
@ -432,7 +591,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
|
new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
|
||||||
{
|
{
|
||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
addControlPoint(rightClickPosition);
|
addControlPoint(lastRightClickPosition);
|
||||||
changeHandler?.EndChange();
|
changeHandler?.EndChange();
|
||||||
}),
|
}),
|
||||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
||||||
|
54
osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs
Normal file
54
osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// 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.Sprites;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Components;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class GenerateToolboxGroup : EditorToolboxGroup
|
||||||
|
{
|
||||||
|
private readonly EditorToolButton polygonButton;
|
||||||
|
|
||||||
|
public GenerateToolboxGroup()
|
||||||
|
: base("Generate")
|
||||||
|
{
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(5),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
polygonButton = new EditorToolButton("Polygon",
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.Spinner },
|
||||||
|
() => new PolygonGenerationPopover()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
if (e.Repeat) return false;
|
||||||
|
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.D:
|
||||||
|
if (!e.ControlPressed || !e.ShiftPressed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
polygonButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,24 +54,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
||||||
=> base.CreateTernaryButtons()
|
=> base.CreateTernaryButtons()
|
||||||
.Concat(DistanceSnapProvider.CreateTernaryButtons())
|
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
|
||||||
.Concat(new[]
|
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||||
{
|
|
||||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })
|
|
||||||
});
|
|
||||||
|
|
||||||
private BindableList<HitObject> selectedHitObjects;
|
private BindableList<HitObject> selectedHitObjects;
|
||||||
|
|
||||||
private Bindable<HitObject> placementObject;
|
private Bindable<HitObject> placementObject;
|
||||||
|
|
||||||
[Cached(typeof(IDistanceSnapProvider))]
|
[Cached(typeof(IDistanceSnapProvider))]
|
||||||
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
|
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();
|
protected readonly FreehandSliderToolboxGroup FreehandSliderToolboxGroup = new FreehandSliderToolboxGroup();
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
@ -110,7 +107,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||||
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||||
},
|
},
|
||||||
FreehandlSliderToolboxGroup
|
new GenerateToolboxGroup(),
|
||||||
|
FreehandSliderToolboxGroup
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class OsuHitObjectInspector : HitObjectInspector
|
public partial class OsuHitObjectInspector : HitObjectInspector
|
||||||
{
|
{
|
||||||
protected override void AddInspectorValues()
|
protected override void AddInspectorValues(HitObject[] objects)
|
||||||
{
|
{
|
||||||
base.AddInspectorValues();
|
base.AddInspectorValues(objects);
|
||||||
|
|
||||||
if (EditorBeatmap.SelectedHitObjects.Count > 0)
|
if (objects.Length > 0)
|
||||||
{
|
{
|
||||||
var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!;
|
var firstInSelection = (OsuHitObject)objects.MinBy(ho => ho.StartTime)!;
|
||||||
var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!;
|
var lastInSelection = (OsuHitObject)objects.MaxBy(ho => ho.GetEndTime())!;
|
||||||
|
|
||||||
Debug.Assert(firstInSelection != null && lastInSelection != null);
|
Debug.Assert(firstInSelection != null && lastInSelection != null);
|
||||||
|
|
||||||
|
193
osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
Normal file
193
osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class PolygonGenerationPopover : OsuPopover
|
||||||
|
{
|
||||||
|
private SliderWithTextBoxInput<double> distanceSnapInput = null!;
|
||||||
|
private SliderWithTextBoxInput<int> offsetAngleInput = null!;
|
||||||
|
private SliderWithTextBoxInput<int> repeatCountInput = null!;
|
||||||
|
private SliderWithTextBoxInput<int> pointInput = null!;
|
||||||
|
private RoundedButton commitButton = null!;
|
||||||
|
|
||||||
|
private readonly List<HitCircle> insertedCircles = new List<HitCircle>();
|
||||||
|
private bool began;
|
||||||
|
private bool committed;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorClock editorClock { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IEditorChangeHandler? changeHandler { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private HitObjectComposer composer { get; set; } = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Width = 220,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(20),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
distanceSnapInput = new SliderWithTextBoxInput<double>("Distance snap:")
|
||||||
|
{
|
||||||
|
Current = new BindableNumber<double>(1)
|
||||||
|
{
|
||||||
|
MinValue = 0.1,
|
||||||
|
MaxValue = 6,
|
||||||
|
Precision = 0.1,
|
||||||
|
Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value,
|
||||||
|
},
|
||||||
|
Instantaneous = true
|
||||||
|
},
|
||||||
|
offsetAngleInput = new SliderWithTextBoxInput<int>("Offset angle:")
|
||||||
|
{
|
||||||
|
Current = new BindableNumber<int>
|
||||||
|
{
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 180,
|
||||||
|
Precision = 1
|
||||||
|
},
|
||||||
|
Instantaneous = true
|
||||||
|
},
|
||||||
|
repeatCountInput = new SliderWithTextBoxInput<int>("Repeats:")
|
||||||
|
{
|
||||||
|
Current = new BindableNumber<int>(1)
|
||||||
|
{
|
||||||
|
MinValue = 1,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 1
|
||||||
|
},
|
||||||
|
Instantaneous = true
|
||||||
|
},
|
||||||
|
pointInput = new SliderWithTextBoxInput<int>("Vertices:")
|
||||||
|
{
|
||||||
|
Current = new BindableNumber<int>(3)
|
||||||
|
{
|
||||||
|
MinValue = 3,
|
||||||
|
MaxValue = 10,
|
||||||
|
Precision = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true
|
||||||
|
},
|
||||||
|
commitButton = new RoundedButton
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Text = "Create",
|
||||||
|
Action = commit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
changeHandler?.BeginChange();
|
||||||
|
began = true;
|
||||||
|
|
||||||
|
distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||||
|
offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||||
|
repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||||
|
pointInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||||
|
tryCreatePolygon();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tryCreatePolygon()
|
||||||
|
{
|
||||||
|
double startTime = beatSnapProvider.SnapTime(editorClock.CurrentTime);
|
||||||
|
TimingControlPoint timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(startTime);
|
||||||
|
double timeSpacing = timingPoint.BeatLength / editorBeatmap.BeatDivisor;
|
||||||
|
IHasSliderVelocity lastWithSliderVelocity = editorBeatmap.HitObjects.Where(ho => ho.GetEndTime() <= startTime).OfType<IHasSliderVelocity>().LastOrDefault() ?? new Slider();
|
||||||
|
double velocity = OsuHitObject.BASE_SCORING_DISTANCE * editorBeatmap.Difficulty.SliderMultiplier
|
||||||
|
/ LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(lastWithSliderVelocity, timingPoint, OsuRuleset.SHORT_NAME);
|
||||||
|
double length = distanceSnapInput.Current.Value * velocity * timeSpacing;
|
||||||
|
float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value)));
|
||||||
|
|
||||||
|
editorBeatmap.RemoveRange(insertedCircles);
|
||||||
|
insertedCircles.Clear();
|
||||||
|
|
||||||
|
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i)
|
||||||
|
{
|
||||||
|
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value);
|
||||||
|
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
|
||||||
|
|
||||||
|
var circle = new HitCircle
|
||||||
|
{
|
||||||
|
Position = position,
|
||||||
|
StartTime = startTime,
|
||||||
|
NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True,
|
||||||
|
};
|
||||||
|
// TODO: probably ensure samples also follow current ternary status (not trivial)
|
||||||
|
circle.Samples.Add(circle.CreateHitSampleInfo());
|
||||||
|
|
||||||
|
if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||||
|
{
|
||||||
|
commitButton.Enabled.Value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertedCircles.Add(circle);
|
||||||
|
startTime = beatSnapProvider.SnapTime(startTime + timeSpacing);
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
editorBeatmap.AddRange(insertedCircles);
|
||||||
|
commitButton.Enabled.Value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void commit()
|
||||||
|
{
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
committed = true;
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
|
if (began && !committed)
|
||||||
|
{
|
||||||
|
editorBeatmap.RemoveRange(insertedCircles);
|
||||||
|
changeHandler?.EndChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ using System.Collections.Generic;
|
|||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
@ -162,6 +163,10 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public SliderTailCircle TailCircle { get; protected set; }
|
public SliderTailCircle TailCircle { get; protected set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[CanBeNull]
|
||||||
|
public SliderRepeat LastRepeat { get; protected set; }
|
||||||
|
|
||||||
public Slider()
|
public Slider()
|
||||||
{
|
{
|
||||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||||
@ -225,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SliderEventType.Repeat:
|
case SliderEventType.Repeat:
|
||||||
AddNested(new SliderRepeat(this)
|
AddNested(LastRepeat = new SliderRepeat(this)
|
||||||
{
|
{
|
||||||
RepeatIndex = e.SpanIndex,
|
RepeatIndex = e.SpanIndex,
|
||||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||||
@ -248,6 +253,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
if (TailCircle != null)
|
if (TailCircle != null)
|
||||||
TailCircle.Position = EndPosition;
|
TailCircle.Position = EndPosition;
|
||||||
|
|
||||||
|
if (LastRepeat != null)
|
||||||
|
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void UpdateNestedSamples()
|
protected void UpdateNestedSamples()
|
||||||
|
@ -359,5 +359,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
return adjustedDifficulty;
|
return adjustedDifficulty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool EditorShowScrollSpeed => false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu
|
namespace osu.Game.Rulesets.Osu
|
||||||
{
|
{
|
||||||
public class OsuSkinComponentLookup : GameplaySkinComponentLookup<OsuSkinComponents>
|
public class OsuSkinComponentLookup : SkinComponentLookup<OsuSkinComponents>
|
||||||
{
|
{
|
||||||
public OsuSkinComponentLookup(OsuSkinComponents component)
|
public OsuSkinComponentLookup(OsuSkinComponents component)
|
||||||
: base(component)
|
: base(component)
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
{
|
{
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
case SkinComponentLookup<HitResult> resultComponent:
|
||||||
HitResult result = resultComponent.Component;
|
HitResult result = resultComponent.Component;
|
||||||
|
|
||||||
// This should eventually be moved to a skin setting, when supported.
|
// This should eventually be moved to a skin setting, when supported.
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
{
|
{
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
case SkinComponentLookup<HitResult> resultComponent:
|
||||||
HitResult result = resultComponent.Component;
|
HitResult result = resultComponent.Component;
|
||||||
|
|
||||||
switch (result)
|
switch (result)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -41,139 +42,187 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
{
|
{
|
||||||
if (lookup is OsuSkinComponentLookup osuComponent)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
switch (osuComponent.Component)
|
case GlobalSkinnableContainerLookup containerLookup:
|
||||||
{
|
// Only handle per ruleset defaults here.
|
||||||
case OsuSkinComponents.FollowPoint:
|
if (containerLookup.Ruleset == null)
|
||||||
return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS));
|
return base.GetDrawableComponent(lookup);
|
||||||
|
|
||||||
case OsuSkinComponents.SliderScorePoint:
|
|
||||||
return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);
|
|
||||||
|
|
||||||
case OsuSkinComponents.SliderFollowCircle:
|
|
||||||
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
|
|
||||||
if (followCircleContent != null)
|
|
||||||
return new LegacyFollowCircle(followCircleContent);
|
|
||||||
|
|
||||||
|
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||||
|
if (!IsProvidingLegacyResources)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.SliderBall:
|
// Our own ruleset components default.
|
||||||
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
|
switch (containerLookup.Lookup)
|
||||||
return new LegacySliderBall(this);
|
{
|
||||||
|
case GlobalSkinnableContainers.MainHUDComponents:
|
||||||
|
return new DefaultSkinComponentsContainer(container =>
|
||||||
|
{
|
||||||
|
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||||
|
|
||||||
return null;
|
if (keyCounter != null)
|
||||||
|
{
|
||||||
|
// set the anchor to top right so that it won't squash to the return button to the top
|
||||||
|
keyCounter.Anchor = Anchor.CentreRight;
|
||||||
|
keyCounter.Origin = Anchor.TopRight;
|
||||||
|
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||||
|
}
|
||||||
|
|
||||||
case OsuSkinComponents.SliderBody:
|
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
||||||
if (hasHitCircle.Value)
|
|
||||||
return new LegacySliderBody();
|
|
||||||
|
|
||||||
return null;
|
if (combo != null)
|
||||||
|
{
|
||||||
|
combo.Anchor = Anchor.BottomLeft;
|
||||||
|
combo.Origin = Anchor.BottomLeft;
|
||||||
|
combo.Scale = new Vector2(1.28f);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new LegacyDefaultComboCounter(),
|
||||||
|
new LegacyKeyCounterDisplay(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case OsuSkinComponents.SliderTailHitCircle:
|
return null;
|
||||||
if (hasHitCircle.Value)
|
|
||||||
return new LegacyMainCirclePiece("sliderendcircle", false);
|
|
||||||
|
|
||||||
return null;
|
case OsuSkinComponentLookup osuComponent:
|
||||||
|
switch (osuComponent.Component)
|
||||||
|
{
|
||||||
|
case OsuSkinComponents.FollowPoint:
|
||||||
|
return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false,
|
||||||
|
maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS));
|
||||||
|
|
||||||
case OsuSkinComponents.SliderHeadHitCircle:
|
case OsuSkinComponents.SliderScorePoint:
|
||||||
if (hasHitCircle.Value)
|
return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);
|
||||||
return new LegacySliderHeadHitCircle();
|
|
||||||
|
|
||||||
return null;
|
case OsuSkinComponents.SliderFollowCircle:
|
||||||
|
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
|
||||||
|
if (followCircleContent != null)
|
||||||
|
return new LegacyFollowCircle(followCircleContent);
|
||||||
|
|
||||||
case OsuSkinComponents.ReverseArrow:
|
|
||||||
if (hasHitCircle.Value)
|
|
||||||
return new LegacyReverseArrow();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case OsuSkinComponents.HitCircle:
|
|
||||||
if (hasHitCircle.Value)
|
|
||||||
return new LegacyMainCirclePiece();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case OsuSkinComponents.Cursor:
|
|
||||||
if (GetTexture("cursor") != null)
|
|
||||||
return new LegacyCursor(this);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case OsuSkinComponents.CursorTrail:
|
|
||||||
if (GetTexture("cursortrail") != null)
|
|
||||||
return new LegacyCursorTrail(this);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case OsuSkinComponents.CursorRipple:
|
|
||||||
if (GetTexture("cursor-ripple") != null)
|
|
||||||
{
|
|
||||||
var ripple = this.GetAnimation("cursor-ripple", false, false);
|
|
||||||
|
|
||||||
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
|
|
||||||
// If anyone complains about these not being applied, this can be uncommented.
|
|
||||||
//
|
|
||||||
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
|
|
||||||
// so we might be okay.
|
|
||||||
//
|
|
||||||
// if (ripple != null)
|
|
||||||
// {
|
|
||||||
// ripple.Scale = new Vector2(0.5f);
|
|
||||||
// ripple.Alpha = 0.2f;
|
|
||||||
// }
|
|
||||||
|
|
||||||
return ripple;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case OsuSkinComponents.CursorParticles:
|
|
||||||
if (GetTexture("star2") != null)
|
|
||||||
return new LegacyCursorParticles();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case OsuSkinComponents.CursorSmoke:
|
|
||||||
if (GetTexture("cursor-smoke") != null)
|
|
||||||
return new LegacySmokeSegment();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case OsuSkinComponents.HitCircleText:
|
|
||||||
if (!this.HasFont(LegacyFont.HitCircle))
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const float hitcircle_text_scale = 0.8f;
|
case OsuSkinComponents.SliderBall:
|
||||||
return new LegacySpriteText(LegacyFont.HitCircle)
|
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
|
||||||
{
|
return new LegacySliderBall(this);
|
||||||
// stable applies a blanket 0.8x scale to hitcircle fonts
|
|
||||||
Scale = new Vector2(hitcircle_text_scale),
|
|
||||||
MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale,
|
|
||||||
};
|
|
||||||
|
|
||||||
case OsuSkinComponents.SpinnerBody:
|
return null;
|
||||||
bool hasBackground = GetTexture("spinner-background") != null;
|
|
||||||
|
|
||||||
if (GetTexture("spinner-top") != null && !hasBackground)
|
case OsuSkinComponents.SliderBody:
|
||||||
return new LegacyNewStyleSpinner();
|
if (hasHitCircle.Value)
|
||||||
else if (hasBackground)
|
return new LegacySliderBody();
|
||||||
return new LegacyOldStyleSpinner();
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.ApproachCircle:
|
case OsuSkinComponents.SliderTailHitCircle:
|
||||||
if (GetTexture(@"approachcircle") != null)
|
if (hasHitCircle.Value)
|
||||||
return new LegacyApproachCircle();
|
return new LegacyMainCirclePiece("sliderendcircle", false);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
default:
|
case OsuSkinComponents.SliderHeadHitCircle:
|
||||||
throw new UnsupportedSkinComponentException(lookup);
|
if (hasHitCircle.Value)
|
||||||
}
|
return new LegacySliderHeadHitCircle();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.ReverseArrow:
|
||||||
|
if (hasHitCircle.Value)
|
||||||
|
return new LegacyReverseArrow();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.HitCircle:
|
||||||
|
if (hasHitCircle.Value)
|
||||||
|
return new LegacyMainCirclePiece();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.Cursor:
|
||||||
|
if (GetTexture("cursor") != null)
|
||||||
|
return new LegacyCursor(this);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.CursorTrail:
|
||||||
|
if (GetTexture("cursortrail") != null)
|
||||||
|
return new LegacyCursorTrail(this);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.CursorRipple:
|
||||||
|
if (GetTexture("cursor-ripple") != null)
|
||||||
|
{
|
||||||
|
var ripple = this.GetAnimation("cursor-ripple", false, false);
|
||||||
|
|
||||||
|
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
|
||||||
|
// If anyone complains about these not being applied, this can be uncommented.
|
||||||
|
//
|
||||||
|
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
|
||||||
|
// so we might be okay.
|
||||||
|
//
|
||||||
|
// if (ripple != null)
|
||||||
|
// {
|
||||||
|
// ripple.Scale = new Vector2(0.5f);
|
||||||
|
// ripple.Alpha = 0.2f;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return ripple;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.CursorParticles:
|
||||||
|
if (GetTexture("star2") != null)
|
||||||
|
return new LegacyCursorParticles();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.CursorSmoke:
|
||||||
|
if (GetTexture("cursor-smoke") != null)
|
||||||
|
return new LegacySmokeSegment();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.HitCircleText:
|
||||||
|
if (!this.HasFont(LegacyFont.HitCircle))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const float hitcircle_text_scale = 0.8f;
|
||||||
|
return new LegacySpriteText(LegacyFont.HitCircle)
|
||||||
|
{
|
||||||
|
// stable applies a blanket 0.8x scale to hitcircle fonts
|
||||||
|
Scale = new Vector2(hitcircle_text_scale),
|
||||||
|
MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale,
|
||||||
|
};
|
||||||
|
|
||||||
|
case OsuSkinComponents.SpinnerBody:
|
||||||
|
bool hasBackground = GetTexture("spinner-background") != null;
|
||||||
|
|
||||||
|
if (GetTexture("spinner-top") != null && !hasBackground)
|
||||||
|
return new LegacyNewStyleSpinner();
|
||||||
|
else if (hasBackground)
|
||||||
|
return new LegacyOldStyleSpinner();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.ApproachCircle:
|
||||||
|
if (GetTexture(@"approachcircle") != null)
|
||||||
|
return new LegacyApproachCircle();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new UnsupportedSkinComponentException(lookup);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return base.GetDrawableComponent(lookup);
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.GetDrawableComponent(lookup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
|
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
|
||||||
|
@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
private double timeOffset;
|
private double timeOffset;
|
||||||
private float time;
|
private float time;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The scale used on creation of a new trail part.
|
||||||
|
/// </summary>
|
||||||
|
public Vector2 NewPartScale = Vector2.One;
|
||||||
|
|
||||||
private Anchor trailOrigin = Anchor.Centre;
|
private Anchor trailOrigin = Anchor.Centre;
|
||||||
|
|
||||||
protected Anchor TrailOrigin
|
protected Anchor TrailOrigin
|
||||||
@ -188,6 +193,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
{
|
{
|
||||||
parts[currentIndex].Position = localSpacePosition;
|
parts[currentIndex].Position = localSpacePosition;
|
||||||
parts[currentIndex].Time = time + 1;
|
parts[currentIndex].Time = time + 1;
|
||||||
|
parts[currentIndex].Scale = NewPartScale;
|
||||||
++parts[currentIndex].InvalidationID;
|
++parts[currentIndex].InvalidationID;
|
||||||
|
|
||||||
currentIndex = (currentIndex + 1) % max_sprites;
|
currentIndex = (currentIndex + 1) % max_sprites;
|
||||||
@ -199,6 +205,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
{
|
{
|
||||||
public Vector2 Position;
|
public Vector2 Position;
|
||||||
public float Time;
|
public float Time;
|
||||||
|
public Vector2 Scale;
|
||||||
public long InvalidationID;
|
public long InvalidationID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
|
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||||
TexturePosition = textureRect.BottomLeft,
|
TexturePosition = textureRect.BottomLeft,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
||||||
@ -289,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
|
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||||
TexturePosition = textureRect.BottomRight,
|
TexturePosition = textureRect.BottomRight,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
||||||
@ -298,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y - texture.DisplayHeight * originPosition.Y),
|
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||||
TexturePosition = textureRect.TopRight,
|
TexturePosition = textureRect.TopRight,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
||||||
@ -307,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y - texture.DisplayHeight * originPosition.Y),
|
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||||
TexturePosition = textureRect.TopLeft,
|
TexturePosition = textureRect.TopLeft,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
||||||
|
@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable;
|
private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current expanded scale of the cursor.
|
||||||
|
/// </summary>
|
||||||
|
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
|
||||||
|
|
||||||
public IBindable<float> CursorScale => cursorScale;
|
public IBindable<float> CursorScale => cursorScale;
|
||||||
|
|
||||||
private readonly Bindable<float> cursorScale = new BindableFloat(1);
|
private readonly Bindable<float> cursorScale = new BindableFloat(1);
|
||||||
|
@ -23,14 +23,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor;
|
public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor;
|
||||||
|
|
||||||
protected override Drawable CreateCursor() => new OsuCursor();
|
protected override Drawable CreateCursor() => new OsuCursor();
|
||||||
|
|
||||||
protected override Container<Drawable> Content => fadeContainer;
|
protected override Container<Drawable> Content => fadeContainer;
|
||||||
|
|
||||||
private readonly Container<Drawable> fadeContainer;
|
private readonly Container<Drawable> fadeContainer;
|
||||||
|
|
||||||
private readonly Bindable<bool> showTrail = new Bindable<bool>(true);
|
private readonly Bindable<bool> showTrail = new Bindable<bool>(true);
|
||||||
|
|
||||||
private readonly Drawable cursorTrail;
|
private readonly SkinnableDrawable cursorTrail;
|
||||||
|
|
||||||
private readonly CursorRippleVisualiser rippleVisualiser;
|
private readonly CursorRippleVisualiser rippleVisualiser;
|
||||||
|
|
||||||
@ -39,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
InternalChild = fadeContainer = new Container
|
InternalChild = fadeContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new[]
|
Children = new CompositeDrawable[]
|
||||||
{
|
{
|
||||||
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
|
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
|
||||||
rippleVisualiser = new CursorRippleVisualiser(),
|
rippleVisualiser = new CursorRippleVisualiser(),
|
||||||
@ -79,6 +78,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
ActiveCursor.Contract();
|
ActiveCursor.Contract();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (cursorTrail.Drawable is CursorTrail trail)
|
||||||
|
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
|
||||||
|
}
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Input.Handlers;
|
using osu.Game.Input.Handlers;
|
||||||
@ -25,18 +26,38 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
{
|
{
|
||||||
public partial class DrawableOsuRuleset : DrawableRuleset<OsuHitObject>
|
public partial class DrawableOsuRuleset : DrawableRuleset<OsuHitObject>
|
||||||
{
|
{
|
||||||
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
|
private Bindable<bool>? cursorHideEnabled;
|
||||||
|
|
||||||
public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;
|
public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;
|
||||||
|
|
||||||
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
|
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
|
||||||
|
|
||||||
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
|
||||||
|
|
||||||
|
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
||||||
: base(ruleset, beatmap, mods)
|
: base(ruleset, beatmap, mods)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override DrawableHitObject<OsuHitObject> CreateDrawableRepresentation(OsuHitObject h) => null;
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ReplayPlayer? replayPlayer)
|
||||||
|
{
|
||||||
|
if (replayPlayer != null)
|
||||||
|
{
|
||||||
|
ReplayAnalysisOverlay analysisOverlay;
|
||||||
|
PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(replayPlayer.Score.Replay));
|
||||||
|
Overlays.Add(analysisOverlay.CreateProxy().With(p => p.Depth = float.NegativeInfinity));
|
||||||
|
replayPlayer.AddSettings(new ReplayAnalysisSettings(Config));
|
||||||
|
|
||||||
|
cursorHideEnabled = Config.GetBindable<bool>(OsuRulesetSetting.ReplayCursorHideEnabled);
|
||||||
|
|
||||||
|
// I have little faith in this working (other things touch cursor visibility) but haven't broken it yet.
|
||||||
|
// Let's wait for someone to report an issue before spending too much time on it.
|
||||||
|
cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override DrawableHitObject<OsuHitObject>? CreateDrawableRepresentation(OsuHitObject h) => null;
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
@ -35,9 +36,11 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
{
|
{
|
||||||
OsuResumeOverlayInputBlocker? inputBlocker = null;
|
OsuResumeOverlayInputBlocker? inputBlocker = null;
|
||||||
|
|
||||||
if (drawableRuleset != null)
|
var drawableOsuRuleset = (DrawableOsuRuleset?)drawableRuleset;
|
||||||
|
|
||||||
|
if (drawableOsuRuleset != null)
|
||||||
{
|
{
|
||||||
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
|
var osuPlayfield = drawableOsuRuleset.Playfield;
|
||||||
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
|
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,13 +48,14 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
{
|
{
|
||||||
Child = clickToResumeCursor = new OsuClickToResumeCursor
|
Child = clickToResumeCursor = new OsuClickToResumeCursor
|
||||||
{
|
{
|
||||||
ResumeRequested = () =>
|
ResumeRequested = action =>
|
||||||
{
|
{
|
||||||
// since the user had to press a button to tap the resume cursor,
|
// since the user had to press a button to tap the resume cursor,
|
||||||
// block that press event from potentially reaching a hit circle that's behind the cursor.
|
// block that press event from potentially reaching a hit circle that's behind the cursor.
|
||||||
// we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one,
|
// we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one,
|
||||||
// so we rely on a dedicated input blocking component that's implanted in there to do that for us.
|
// so we rely on a dedicated input blocking component that's implanted in there to do that for us.
|
||||||
if (inputBlocker != null)
|
// note this only matters when the user didn't pause while they were holding the same key that they are resuming with.
|
||||||
|
if (inputBlocker != null && !drawableOsuRuleset.AsNonNull().KeyBindingInputManager.PressedActions.Contains(action))
|
||||||
inputBlocker.BlockNextPress = true;
|
inputBlocker.BlockNextPress = true;
|
||||||
|
|
||||||
Resume();
|
Resume();
|
||||||
@ -94,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
{
|
{
|
||||||
public override bool HandlePositionalInput => true;
|
public override bool HandlePositionalInput => true;
|
||||||
|
|
||||||
public Action? ResumeRequested;
|
public Action<OsuAction>? ResumeRequested;
|
||||||
private Container scaleTransitionContainer = null!;
|
private Container scaleTransitionContainer = null!;
|
||||||
|
|
||||||
public OsuClickToResumeCursor()
|
public OsuClickToResumeCursor()
|
||||||
@ -136,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
|
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
|
||||||
ResumeRequested?.Invoke();
|
ResumeRequested?.Invoke(e.Action);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Graphics.Performance;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||||
|
{
|
||||||
|
public partial class AnalysisFrameEntry : LifetimeEntry
|
||||||
|
{
|
||||||
|
public OsuAction[] Action { get; }
|
||||||
|
|
||||||
|
public Vector2 Position { get; }
|
||||||
|
|
||||||
|
public AnalysisFrameEntry(double time, double displayLength, Vector2 position, params OsuAction[] action)
|
||||||
|
{
|
||||||
|
LifetimeStart = time;
|
||||||
|
LifetimeEnd = time + displayLength;
|
||||||
|
Position = position;
|
||||||
|
Action = action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs
Normal file
28
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Rulesets.Objects.Pooling;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||||
|
{
|
||||||
|
public abstract partial class AnalysisMarker : PoolableDrawableWithLifetime<AnalysisFrameEntry>
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
protected OsuColour Colours { get; private set; } = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnApply(AnalysisFrameEntry entry)
|
||||||
|
{
|
||||||
|
Position = entry.Position;
|
||||||
|
Depth = -(float)entry.LifetimeEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs
Normal file
88
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A marker which shows one click, with visuals focusing on the button which was clicked and the precise location of the click.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ClickMarker : AnalysisMarker
|
||||||
|
{
|
||||||
|
private CircularProgress leftClickDisplay = null!;
|
||||||
|
private CircularProgress rightClickDisplay = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Circle
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(0.125f),
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive,
|
||||||
|
Colour = Colours.Gray5,
|
||||||
|
},
|
||||||
|
new CircularContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Colours.Gray5,
|
||||||
|
Masking = true,
|
||||||
|
BorderThickness = 2.2f,
|
||||||
|
BorderColour = Color4.White,
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
Colour = Color4.Black,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
AlwaysPresent = true,
|
||||||
|
Alpha = 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
leftClickDisplay = new CircularProgress
|
||||||
|
{
|
||||||
|
Colour = Colours.Yellow,
|
||||||
|
Size = new Vector2(0.95f),
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Rotation = 180,
|
||||||
|
Progress = 0.5f,
|
||||||
|
InnerRadius = 0.18f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
rightClickDisplay = new CircularProgress
|
||||||
|
{
|
||||||
|
Colour = Colours.Yellow,
|
||||||
|
Size = new Vector2(0.95f),
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Progress = 0.5f,
|
||||||
|
InnerRadius = 0.18f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Size = new Vector2(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnApply(AnalysisFrameEntry entry)
|
||||||
|
{
|
||||||
|
base.OnApply(entry);
|
||||||
|
|
||||||
|
leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0;
|
||||||
|
rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
// 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.Pooling;
|
||||||
|
using osu.Game.Rulesets.Objects.Pooling;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||||
|
{
|
||||||
|
public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer<AnalysisFrameEntry, AnalysisMarker>
|
||||||
|
{
|
||||||
|
private readonly DrawablePool<ClickMarker> clickMarkerPool;
|
||||||
|
|
||||||
|
public ClickMarkerContainer()
|
||||||
|
{
|
||||||
|
AddInternal(clickMarkerPool = new DrawablePool<ClickMarker>(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Graphics.Lines;
|
||||||
|
using osu.Framework.Graphics.Performance;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||||
|
{
|
||||||
|
public partial class CursorPathContainer : Path
|
||||||
|
{
|
||||||
|
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
|
||||||
|
private readonly SortedSet<AnalysisFrameEntry> aliveEntries = new SortedSet<AnalysisFrameEntry>(new AimLinePointComparator());
|
||||||
|
|
||||||
|
public CursorPathContainer()
|
||||||
|
{
|
||||||
|
lifetimeManager.EntryBecameAlive += entryBecameAlive;
|
||||||
|
lifetimeManager.EntryBecameDead += entryBecameDead;
|
||||||
|
|
||||||
|
PathRadius = 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
Colour = colours.Pink2;
|
||||||
|
BackgroundColour = colours.Pink2.Opacity(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
lifetimeManager.Update(Time.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(AnalysisFrameEntry entry) => lifetimeManager.AddEntry(entry);
|
||||||
|
|
||||||
|
private void entryBecameAlive(LifetimeEntry entry)
|
||||||
|
{
|
||||||
|
aliveEntries.Add((AnalysisFrameEntry)entry);
|
||||||
|
updateVertices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void entryBecameDead(LifetimeEntry entry)
|
||||||
|
{
|
||||||
|
aliveEntries.Remove((AnalysisFrameEntry)entry);
|
||||||
|
updateVertices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateVertices()
|
||||||
|
{
|
||||||
|
ClearVertices();
|
||||||
|
|
||||||
|
Vector2 min = Vector2.Zero;
|
||||||
|
|
||||||
|
foreach (var entry in aliveEntries)
|
||||||
|
{
|
||||||
|
AddVertex(entry.Position);
|
||||||
|
if (entry.Position.X < min.X)
|
||||||
|
min.X = entry.Position.X;
|
||||||
|
|
||||||
|
if (entry.Position.Y < min.Y)
|
||||||
|
min.Y = entry.Position.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
Position = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class AimLinePointComparator : IComparer<AnalysisFrameEntry>
|
||||||
|
{
|
||||||
|
public int Compare(AnalysisFrameEntry? x, AnalysisFrameEntry? y)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(x);
|
||||||
|
ArgumentNullException.ThrowIfNull(y);
|
||||||
|
|
||||||
|
return x.LifetimeStart.CompareTo(y.LifetimeStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs
Normal file
69
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A marker which shows one movement frame, include any buttons which are pressed.
|
||||||
|
/// </summary>
|
||||||
|
public partial class FrameMarker : AnalysisMarker
|
||||||
|
{
|
||||||
|
private CircularProgress leftClickDisplay = null!;
|
||||||
|
private CircularProgress rightClickDisplay = null!;
|
||||||
|
private Circle mainCircle = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
mainCircle = new Circle
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Colours.Pink2,
|
||||||
|
},
|
||||||
|
leftClickDisplay = new CircularProgress
|
||||||
|
{
|
||||||
|
Colour = Colours.Yellow,
|
||||||
|
Size = new Vector2(0.8f),
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Rotation = 180,
|
||||||
|
Progress = 0.5f,
|
||||||
|
InnerRadius = 0.5f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
rightClickDisplay = new CircularProgress
|
||||||
|
{
|
||||||
|
Colour = Colours.Yellow,
|
||||||
|
Size = new Vector2(0.8f),
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Progress = 0.5f,
|
||||||
|
InnerRadius = 0.5f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnApply(AnalysisFrameEntry entry)
|
||||||
|
{
|
||||||
|
base.OnApply(entry);
|
||||||
|
Size = new Vector2(entry.Action.Any() ? 4 : 2.5f);
|
||||||
|
|
||||||
|
mainCircle.Colour = entry.Action.Any() ? Colours.Gray4 : Colours.Pink2;
|
||||||
|
|
||||||
|
leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0;
|
||||||
|
rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
// 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.Pooling;
|
||||||
|
using osu.Game.Rulesets.Objects.Pooling;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||||
|
{
|
||||||
|
public partial class FrameMarkerContainer : PooledDrawableWithLifetimeContainer<AnalysisFrameEntry, AnalysisMarker>
|
||||||
|
{
|
||||||
|
private readonly DrawablePool<FrameMarker> pool;
|
||||||
|
|
||||||
|
public FrameMarkerContainer()
|
||||||
|
{
|
||||||
|
AddInternal(pool = new DrawablePool<FrameMarker>(80));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry));
|
||||||
|
}
|
||||||
|
}
|
128
osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs
Normal file
128
osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Caching;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
|
using osu.Game.Rulesets.Osu.UI.ReplayAnalysis;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
|
{
|
||||||
|
public partial class ReplayAnalysisOverlay : CompositeDrawable
|
||||||
|
{
|
||||||
|
private BindableBool showClickMarkers { get; } = new BindableBool();
|
||||||
|
private BindableBool showFrameMarkers { get; } = new BindableBool();
|
||||||
|
private BindableBool showCursorPath { get; } = new BindableBool();
|
||||||
|
private BindableInt displayLength { get; } = new BindableInt();
|
||||||
|
|
||||||
|
protected ClickMarkerContainer? ClickMarkers;
|
||||||
|
protected FrameMarkerContainer? FrameMarkers;
|
||||||
|
protected CursorPathContainer? CursorPath;
|
||||||
|
|
||||||
|
private readonly Replay replay;
|
||||||
|
|
||||||
|
public ReplayAnalysisOverlay(Replay replay)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
this.replay = replay;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool requireDisplay => showClickMarkers.Value || showFrameMarkers.Value || showCursorPath.Value;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuRulesetConfigManager config)
|
||||||
|
{
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, showClickMarkers);
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, showFrameMarkers);
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, showCursorPath);
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, displayLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
displayLength.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
// Need to fully reload to make this work.
|
||||||
|
loaded.Invalidate();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Cached loaded = new Cached();
|
||||||
|
|
||||||
|
private CancellationTokenSource? generationCancellationSource;
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (requireDisplay)
|
||||||
|
initialise();
|
||||||
|
|
||||||
|
if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0;
|
||||||
|
if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0;
|
||||||
|
if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialise()
|
||||||
|
{
|
||||||
|
if (loaded.IsValid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
loaded.Validate();
|
||||||
|
|
||||||
|
generationCancellationSource?.Cancel();
|
||||||
|
generationCancellationSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
// It's faster to reinitialise the whole drawable stack than use `Clear` on `PooledDrawableWithLifetimeContainer`
|
||||||
|
var newDrawables = new Drawable[]
|
||||||
|
{
|
||||||
|
CursorPath = new CursorPathContainer(),
|
||||||
|
ClickMarkers = new ClickMarkerContainer(),
|
||||||
|
FrameMarkers = new FrameMarkerContainer(),
|
||||||
|
};
|
||||||
|
|
||||||
|
bool leftHeld = false;
|
||||||
|
bool rightHeld = false;
|
||||||
|
|
||||||
|
// This should probably be async as well, but it's a bit of a pain to debounce and everything.
|
||||||
|
// Let's address concerns when they are raised.
|
||||||
|
foreach (var frame in replay.Frames)
|
||||||
|
{
|
||||||
|
var osuFrame = (OsuReplayFrame)frame;
|
||||||
|
|
||||||
|
bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton);
|
||||||
|
bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton);
|
||||||
|
|
||||||
|
if (leftHeld && !leftButton)
|
||||||
|
leftHeld = false;
|
||||||
|
else if (!leftHeld && leftButton)
|
||||||
|
{
|
||||||
|
leftHeld = true;
|
||||||
|
ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.LeftButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightHeld && !rightButton)
|
||||||
|
rightHeld = false;
|
||||||
|
else if (!rightHeld && rightButton)
|
||||||
|
{
|
||||||
|
rightHeld = true;
|
||||||
|
ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.RightButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray()));
|
||||||
|
CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position));
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadComponentsAsync(newDrawables, drawables => InternalChildrenEnumerable = drawables, generationCancellationSource.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
55
osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs
Normal file
55
osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Osu.Configuration;
|
||||||
|
using osu.Game.Screens.Play.PlayerSettings;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
|
{
|
||||||
|
public partial class ReplayAnalysisSettings : PlayerSettingsGroup
|
||||||
|
{
|
||||||
|
private readonly OsuRulesetConfigManager config;
|
||||||
|
|
||||||
|
[SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))]
|
||||||
|
public BindableBool ShowClickMarkers { get; } = new BindableBool();
|
||||||
|
|
||||||
|
[SettingSource("Show frame markers", SettingControlType = typeof(PlayerCheckbox))]
|
||||||
|
public BindableBool ShowAimMarkers { get; } = new BindableBool();
|
||||||
|
|
||||||
|
[SettingSource("Show cursor path", SettingControlType = typeof(PlayerCheckbox))]
|
||||||
|
public BindableBool ShowCursorPath { get; } = new BindableBool();
|
||||||
|
|
||||||
|
[SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))]
|
||||||
|
public BindableBool HideSkinCursor { get; } = new BindableBool();
|
||||||
|
|
||||||
|
[SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar<int>))]
|
||||||
|
public BindableInt DisplayLength { get; } = new BindableInt
|
||||||
|
{
|
||||||
|
MinValue = 200,
|
||||||
|
MaxValue = 2000,
|
||||||
|
Default = 800,
|
||||||
|
Precision = 200,
|
||||||
|
};
|
||||||
|
|
||||||
|
public ReplayAnalysisSettings(OsuRulesetConfigManager config)
|
||||||
|
: base("Analysis Settings")
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AddRange(this.CreateSettingsControls());
|
||||||
|
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, ShowClickMarkers);
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers);
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath);
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor);
|
||||||
|
config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, DisplayLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,12 +21,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
public class TaikoDifficultyCalculator : DifficultyCalculator
|
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const double difficulty_multiplier = 1.35;
|
private const double difficulty_multiplier = 0.084375;
|
||||||
|
private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier;
|
||||||
private const double final_multiplier = 0.0625;
|
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||||
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
|
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||||
private const double colour_skill_multiplier = 0.375 * final_multiplier;
|
|
||||||
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
|
|
||||||
|
|
||||||
public override int Version => 20221107;
|
public override int Version => 20221107;
|
||||||
|
|
||||||
@ -83,11 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||||
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
||||||
|
|
||||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier;
|
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier;
|
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier;
|
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||||
|
|
||||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier;
|
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
|
||||||
double starRating = rescale(combinedRating * 1.4);
|
double starRating = rescale(combinedRating * 1.4);
|
||||||
|
|
||||||
HitWindows hitWindows = new TaikoHitWindows();
|
HitWindows hitWindows = new TaikoHitWindows();
|
||||||
|
@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup component)
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
{
|
{
|
||||||
switch (component)
|
switch (lookup)
|
||||||
{
|
{
|
||||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
case SkinComponentLookup<HitResult> resultComponent:
|
||||||
// This should eventually be moved to a skin setting, when supported.
|
// This should eventually be moved to a skin setting, when supported.
|
||||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||||
return Drawable.Empty();
|
return Drawable.Empty();
|
||||||
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.GetDrawableComponent(component);
|
return base.GetDrawableComponent(lookup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
|
|
||||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||||
{
|
{
|
||||||
if (lookup is GameplaySkinComponentLookup<HitResult>)
|
if (lookup is SkinComponentLookup<HitResult>)
|
||||||
{
|
{
|
||||||
// if a taiko skin is providing explosion sprites, hide the judgements completely
|
// if a taiko skin is providing explosion sprites, hide the judgements completely
|
||||||
if (hasExplosion.Value)
|
if (hasExplosion.Value)
|
||||||
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko
|
namespace osu.Game.Rulesets.Taiko
|
||||||
{
|
{
|
||||||
public class TaikoSkinComponentLookup : GameplaySkinComponentLookup<TaikoSkinComponents>
|
public class TaikoSkinComponentLookup : SkinComponentLookup<TaikoSkinComponents>
|
||||||
{
|
{
|
||||||
public TaikoSkinComponentLookup(TaikoSkinComponents component)
|
public TaikoSkinComponentLookup(TaikoSkinComponents component)
|
||||||
: base(component)
|
: base(component)
|
||||||
|
@ -468,6 +468,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDecodeBeatmapHitObjectCoordinatesLegacy()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder();
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("hitobject-coordinates-legacy.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var hitObjects = decoder.Decode(stream).HitObjects;
|
||||||
|
|
||||||
|
var positionData = hitObjects[0] as IHasPosition;
|
||||||
|
|
||||||
|
Assert.IsNotNull(positionData);
|
||||||
|
Assert.AreEqual(new Vector2(256, 256), positionData!.Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDecodeBeatmapHitObjectCoordinatesLazer()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION);
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("hitobject-coordinates-lazer.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var hitObjects = decoder.Decode(stream).HitObjects;
|
||||||
|
|
||||||
|
var positionData = hitObjects[0] as IHasPosition;
|
||||||
|
|
||||||
|
Assert.IsNotNull(positionData);
|
||||||
|
Assert.AreEqual(new Vector2(256.99853f, 256.001f), positionData!.Position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDecodeBeatmapHitObjects()
|
public void TestDecodeBeatmapHitObjects()
|
||||||
{
|
{
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user