mirror of
https://github.com/ppy/osu.git
synced 2024-11-14 14:27:25 +08:00
Merge branch 'master' into beatmap-info-purge
This commit is contained in:
commit
58fe502af4
@ -21,7 +21,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ppy.localisationanalyser.tools": {
|
"ppy.localisationanalyser.tools": {
|
||||||
"version": "2024.517.0",
|
"version": "2024.802.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"localisation"
|
"localisation"
|
||||||
]
|
]
|
||||||
|
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -64,7 +64,8 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- { prettyname: Windows, fullname: windows-latest }
|
- { prettyname: Windows, fullname: windows-latest }
|
||||||
- { prettyname: macOS, fullname: macos-latest }
|
# macOS runner performance has gotten unbearably slow so let's turn them off temporarily.
|
||||||
|
# - { prettyname: macOS, fullname: macos-latest }
|
||||||
- { prettyname: Linux, fullname: ubuntu-latest }
|
- { prettyname: Linux, fullname: ubuntu-latest }
|
||||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
threadingMode: ['SingleThread', 'MultiThreaded']
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
@ -120,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
|
||||||
@ -136,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
|
||||||
|
@ -55,7 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
|
|||||||
|
|
||||||
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
||||||
|
|
||||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
||||||
|
|
||||||
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.720.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,5 +13,7 @@ namespace osu.Desktop.Windows
|
|||||||
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
||||||
|
|
||||||
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
||||||
|
|
||||||
|
public static string Beatmap => Path.Join(icon_directory, "beatmap.ico");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,10 +40,10 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
private static readonly FileAssociation[] file_associations =
|
private static readonly FileAssociation[] file_associations =
|
||||||
{
|
{
|
||||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap),
|
||||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap),
|
||||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
|
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Beatmap),
|
||||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
|
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Beatmap),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly UriAssociation[] uri_associations =
|
private static readonly UriAssociation[] uri_associations =
|
||||||
|
BIN
osu.Desktop/beatmap.ico
Normal file
BIN
osu.Desktop/beatmap.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 349 KiB |
@ -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" />
|
||||||
|
@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
|
|
||||||
AddMouseMoveStep(-100, 100);
|
AddMouseMoveStep(-100, 100);
|
||||||
addVertexCheckStep(3, 1, times[0], positions[0]);
|
addVertexCheckStep(3, 1, times[0], positions[0]);
|
||||||
|
addDragEndStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
|
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
|
||||||
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
|
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
|
||||||
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
|
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
|
||||||
|
|
||||||
|
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
|
||||||
|
addDragEndStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addDragStartStep(times[1], positions[1]);
|
addDragStartStep(times[1], positions[1]);
|
||||||
AddMouseMoveStep(times[1], 400);
|
AddMouseMoveStep(times[1], 400);
|
||||||
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
|
||||||
|
addDragEndStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -129,6 +134,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
|
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
|
||||||
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
|
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
|
||||||
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
|
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
|
||||||
|
addDragEndStep();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -161,18 +167,18 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addAddVertexSteps(500, 150);
|
addAddVertexSteps(500, 150);
|
||||||
addVertexCheckStep(3, 1, 500, 150);
|
addVertexCheckStep(3, 1, 500, 150);
|
||||||
|
|
||||||
addAddVertexSteps(90, 200);
|
addAddVertexSteps(160, 200);
|
||||||
addVertexCheckStep(4, 1, times[0], positions[0]);
|
addVertexCheckStep(4, 1, 160, 200);
|
||||||
|
|
||||||
addAddVertexSteps(750, 180);
|
addAddVertexSteps(750, 180);
|
||||||
addVertexCheckStep(5, 4, 750, 180);
|
addVertexCheckStep(5, 4, 800, 160);
|
||||||
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
|
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDeleteVertex()
|
public void TestDeleteVertex()
|
||||||
{
|
{
|
||||||
double[] times = { 100, 300, 500 };
|
double[] times = { 100, 300, 400 };
|
||||||
float[] positions = { 100, 200, 150 };
|
float[] positions = { 100, 200, 150 };
|
||||||
addBlueprintStep(times, positions);
|
addBlueprintStep(times, positions);
|
||||||
|
|
||||||
@ -265,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
AddStep("delete vertex", () =>
|
AddStep("delete vertex", () =>
|
||||||
{
|
{
|
||||||
InputManager.PressKey(Key.ShiftLeft);
|
InputManager.PressKey(Key.ShiftLeft);
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Right);
|
||||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Rulesets.Catch.Objects;
|
|||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||||
@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private IBeatSnapProvider? beatSnapProvider { get; set; }
|
private IBeatSnapProvider? beatSnapProvider { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
protected EditorBeatmap? EditorBeatmap { get; private set; }
|
||||||
|
|
||||||
protected EditablePath(Func<float, double> positionToTime)
|
protected EditablePath(Func<float, double> positionToTime)
|
||||||
{
|
{
|
||||||
PositionToTime = positionToTime;
|
PositionToTime = positionToTime;
|
||||||
@ -103,15 +107,23 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
//
|
//
|
||||||
// The value is clamped here by the bindable min and max values.
|
// The value is clamped here by the bindable min and max values.
|
||||||
// In case the required velocity is too large, the path is not preserved.
|
// In case the required velocity is too large, the path is not preserved.
|
||||||
|
double previousVelocity = svBindable.Value;
|
||||||
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
|
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
|
||||||
|
|
||||||
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
|
// adjust velocity locally, so that once the SV change is applied by applying defaults
|
||||||
|
// (triggered by `EditorBeatmap.Update()` call at end of method),
|
||||||
|
// it results in the outcome desired by the user.
|
||||||
|
double relativeChange = svBindable.Value / previousVelocity;
|
||||||
|
double localVelocity = hitObject.Velocity * relativeChange;
|
||||||
|
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, localVelocity);
|
||||||
|
|
||||||
if (beatSnapProvider == null) return;
|
if (beatSnapProvider == null) return;
|
||||||
|
|
||||||
double endTime = hitObject.StartTime + path.Duration;
|
double endTime = hitObject.StartTime + path.Duration;
|
||||||
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
|
||||||
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
|
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * localVelocity;
|
||||||
|
|
||||||
|
EditorBeatmap?.Update(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
|
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)
|
||||||
|
@ -4,12 +4,11 @@
|
|||||||
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.Graphics.Cursor;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -19,22 +18,27 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
{
|
{
|
||||||
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
|
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
|
||||||
|
|
||||||
|
private readonly JuiceStream juiceStream;
|
||||||
|
|
||||||
// To handle when the editor is scrolled while dragging.
|
// To handle when the editor is scrolled while dragging.
|
||||||
private Vector2 dragStartPosition;
|
private Vector2 dragStartPosition;
|
||||||
|
|
||||||
[Resolved]
|
public SelectionEditablePath(JuiceStream juiceStream, Func<float, double> positionToTime)
|
||||||
private IEditorChangeHandler? changeHandler { get; set; }
|
|
||||||
|
|
||||||
public SelectionEditablePath(Func<float, double> positionToTime)
|
|
||||||
: base(positionToTime)
|
: base(positionToTime)
|
||||||
{
|
{
|
||||||
|
this.juiceStream = juiceStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddVertex(Vector2 relativePosition)
|
public void AddVertex(Vector2 relativePosition)
|
||||||
{
|
{
|
||||||
|
EditorBeatmap?.BeginChange();
|
||||||
|
|
||||||
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
double time = Math.Max(0, PositionToTime(relativePosition.Y));
|
||||||
int index = AddVertex(time, relativePosition.X);
|
int index = AddVertex(time, relativePosition.X);
|
||||||
|
UpdateHitObjectFromPath(juiceStream);
|
||||||
selectOnly(index);
|
selectOnly(index);
|
||||||
|
|
||||||
|
EditorBeatmap?.EndChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
|
||||||
@ -45,9 +49,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
if (index == -1 || VertexStates[index].IsFixed)
|
if (index == -1 || VertexStates[index].IsFixed)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (e.Button == MouseButton.Left && e.ShiftPressed)
|
if (e.Button == MouseButton.Right && e.ShiftPressed)
|
||||||
{
|
{
|
||||||
|
EditorBeatmap?.BeginChange();
|
||||||
RemoveVertex(index);
|
RemoveVertex(index);
|
||||||
|
UpdateHitObjectFromPath(juiceStream);
|
||||||
|
EditorBeatmap?.EndChange();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
for (int i = 0; i < VertexCount; i++)
|
for (int i = 0; i < VertexCount; i++)
|
||||||
VertexStates[i].VertexBeforeChange = Vertices[i];
|
VertexStates[i].VertexBeforeChange = Vertices[i];
|
||||||
|
|
||||||
changeHandler?.BeginChange();
|
EditorBeatmap?.BeginChange();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e)
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
{
|
{
|
||||||
changeHandler?.EndChange();
|
EditorBeatmap?.EndChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getMouseTargetVertex(Vector2 screenSpacePosition)
|
private int getMouseTargetVertex(Vector2 screenSpacePosition)
|
||||||
@ -118,11 +126,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
private void deleteSelectedVertices()
|
private void deleteSelectedVertices()
|
||||||
{
|
{
|
||||||
|
EditorBeatmap?.BeginChange();
|
||||||
|
|
||||||
for (int i = VertexCount - 1; i >= 0; i--)
|
for (int i = VertexCount - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if (VertexStates[i].IsSelected)
|
if (VertexStates[i].IsSelected)
|
||||||
RemoveVertex(i);
|
RemoveVertex(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateHitObjectFromPath(juiceStream);
|
||||||
|
|
||||||
|
EditorBeatmap?.EndChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
{
|
{
|
||||||
public partial class VertexPiece : Circle
|
public partial class VertexPiece : Circle
|
||||||
{
|
{
|
||||||
|
private VertexState state = new VertexState();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour osuColour { get; set; } = null!;
|
private OsuColour osuColour { get; set; } = null!;
|
||||||
|
|
||||||
@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
public void UpdateFrom(VertexState state)
|
public void UpdateFrom(VertexState state)
|
||||||
{
|
{
|
||||||
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow;
|
this.state = state;
|
||||||
|
updateMarkerDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
updateMarkerDisplay();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
updateMarkerDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the state of the circular control point marker.
|
||||||
|
/// </summary>
|
||||||
|
private void updateMarkerDisplay()
|
||||||
|
{
|
||||||
|
var colour = osuColour.Yellow;
|
||||||
|
|
||||||
|
if (IsHovered || state.IsSelected)
|
||||||
|
colour = colour.Lighten(1);
|
||||||
|
|
||||||
|
Colour = colour;
|
||||||
Alpha = state.IsFixed ? 0.5f : 1;
|
Alpha = state.IsFixed ? 0.5f : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
{
|
{
|
||||||
scrollingPath = new ScrollingPath(),
|
scrollingPath = new ScrollingPath(),
|
||||||
nestedOutlineContainer = new NestedOutlineContainer(),
|
nestedOutlineContainer = new NestedOutlineContainer(),
|
||||||
editablePath = new SelectionEditablePath(positionToTime)
|
editablePath = new SelectionEditablePath(hitObject, positionToTime)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Objects
|
namespace osu.Game.Rulesets.Catch.Objects
|
||||||
{
|
{
|
||||||
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
|
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt
|
||||||
{
|
{
|
||||||
public const float OBJECT_RADIUS = 64;
|
public const float OBJECT_RADIUS = 64;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
{
|
{
|
||||||
base.PostProcess();
|
base.PostProcess();
|
||||||
|
|
||||||
var hitObjects = Beatmap.HitObjects as List<OsuHitObject> ?? Beatmap.HitObjects.OfType<OsuHitObject>().ToList();
|
ApplyStacking(Beatmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ApplyStacking(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
var hitObjects = beatmap.HitObjects as List<OsuHitObject> ?? beatmap.HitObjects.OfType<OsuHitObject>().ToList();
|
||||||
|
|
||||||
if (hitObjects.Count > 0)
|
if (hitObjects.Count > 0)
|
||||||
{
|
{
|
||||||
@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
foreach (var h in hitObjects)
|
foreach (var h in hitObjects)
|
||||||
h.StackHeight = 0;
|
h.StackHeight = 0;
|
||||||
|
|
||||||
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
|
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
|
||||||
applyStacking(Beatmap, hitObjects, 0, hitObjects.Count - 1);
|
applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1);
|
||||||
else
|
else
|
||||||
applyStackingOld(Beatmap, hitObjects);
|
applyStackingOld(beatmap, hitObjects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyStacking(IBeatmap beatmap, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
|
private static void applyStacking(IBeatmap beatmap, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
|
||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||||
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
|
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
|
||||||
@ -209,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyStackingOld(IBeatmap beatmap, List<OsuHitObject> hitObjects)
|
private static void applyStackingOld(IBeatmap beatmap, List<OsuHitObject> hitObjects)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < hitObjects.Count; i++)
|
for (int i = 0; i < hitObjects.Count; i++)
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
|
|
||||||
private double skillMultiplier => 23.55;
|
private double skillMultiplier => 24.963;
|
||||||
private double strainDecayBase => 0.15;
|
private double strainDecayBase => 0.15;
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
hasHiddenMod = mods.Any(m => m is OsuModHidden);
|
hasHiddenMod = mods.Any(m => m is OsuModHidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double skillMultiplier => 0.052;
|
private double skillMultiplier => 0.05512;
|
||||||
private double strainDecayBase => 0.15;
|
private double strainDecayBase => 0.15;
|
||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
@ -41,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
return currentStrain;
|
return currentStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
|
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
|
||||||
|
|
||||||
|
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
{
|
{
|
||||||
public abstract class OsuStrainSkill : StrainSkill
|
public abstract class OsuStrainSkill : StrainSkill
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The default multiplier applied by <see cref="OsuStrainSkill"/> to the final difficulty value after all other calculations.
|
|
||||||
/// May be overridden via <see cref="DifficultyMultiplier"/>.
|
|
||||||
/// </summary>
|
|
||||||
public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||||
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
||||||
@ -29,11 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual double ReducedStrainBaseline => 0.75;
|
protected virtual double ReducedStrainBaseline => 0.75;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER;
|
|
||||||
|
|
||||||
protected OsuStrainSkill(Mod[] mods)
|
protected OsuStrainSkill(Mod[] mods)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
@ -65,7 +54,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
weight *= DecayWeight;
|
weight *= DecayWeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return difficulty * DifficultyMultiplier;
|
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,14 +16,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : OsuStrainSkill
|
public class Speed : OsuStrainSkill
|
||||||
{
|
{
|
||||||
private double skillMultiplier => 1375;
|
private double skillMultiplier => 1.430;
|
||||||
private double strainDecayBase => 0.3;
|
private double strainDecayBase => 0.3;
|
||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
private double currentRhythm;
|
private double currentRhythm;
|
||||||
|
|
||||||
protected override int ReducedSectionCount => 5;
|
protected override int ReducedSectionCount => 5;
|
||||||
protected override double DifficultyMultiplier => 1.04;
|
|
||||||
|
|
||||||
private readonly List<double> objectStrains = new List<double>();
|
private readonly List<double> objectStrains = new List<double>();
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -295,6 +293,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
|
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
|
||||||
{
|
{
|
||||||
|
// if the snap target is a stacked object, snap to its unstacked position rather than its stacked position.
|
||||||
|
// this is intended to make working with stacks easier (because thanks to this, you can drag an object to any
|
||||||
|
// of the items on the stack to add an object to it, rather than having to drag to the position of the *first* object on it at all times).
|
||||||
|
if (b.Item is OsuHitObject osuObject && osuObject.StackOffset != Vector2.Zero)
|
||||||
|
closestSnapPosition = b.ToScreenSpace(b.ToLocalSpace(closestSnapPosition) - osuObject.StackOffset);
|
||||||
|
|
||||||
// only return distance portion, since time is not really valid
|
// only return distance portion, since time is not really valid
|
||||||
snapResult = new SnapResult(closestSnapPosition, null, playfield);
|
snapResult = new SnapResult(closestSnapPosition, null, playfield);
|
||||||
return true;
|
return true;
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
@ -50,12 +51,33 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
var hitObjects = selectedMovableObjects;
|
var hitObjects = selectedMovableObjects;
|
||||||
|
|
||||||
|
var localDelta = this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
|
||||||
|
|
||||||
|
// this conditional is a rather ugly special case for stacks.
|
||||||
|
// as it turns out, adding the `EditorBeatmap.Update()` call at the end of this would cause stacked objects to jitter when moved around
|
||||||
|
// (they would stack and then unstack every frame).
|
||||||
|
// the reason for that is that the selection handling abstractions are not aware of the distinction between "displayed" and "actual" position
|
||||||
|
// which is unique to osu! due to stacking being applied as a post-processing step.
|
||||||
|
// therefore, the following loop would occur:
|
||||||
|
// - on frame 1 the blueprint is snapped to the stack's baseline position. `EditorBeatmap.Update()` applies stacking successfully,
|
||||||
|
// the blueprint moves up the stack from its original drag position.
|
||||||
|
// - on frame 2 the blueprint's position is now the *stacked* position, which is interpreted higher up as *manually performing an unstack*
|
||||||
|
// to the blueprint's unstacked position (as the machinery higher up only cares about differences in screen space position).
|
||||||
|
if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset)))
|
||||||
|
return true;
|
||||||
|
|
||||||
// this will potentially move the selection out of bounds...
|
// this will potentially move the selection out of bounds...
|
||||||
foreach (var h in hitObjects)
|
foreach (var h in hitObjects)
|
||||||
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
|
h.Position += localDelta;
|
||||||
|
|
||||||
// but this will be corrected.
|
// but this will be corrected.
|
||||||
moveSelectionInBounds();
|
moveSelectionInBounds();
|
||||||
|
|
||||||
|
// manually update stacking.
|
||||||
|
// this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons,
|
||||||
|
// as the entire flow is too expensive to run on every movement.
|
||||||
|
Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,19 +5,23 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
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;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
public partial class DrawableOsuJudgement : DrawableJudgement
|
public partial class DrawableOsuJudgement : DrawableJudgement
|
||||||
{
|
{
|
||||||
|
internal Color4 AccentColour { get; private set; }
|
||||||
|
|
||||||
internal SkinnableLighting Lighting { get; private set; } = null!;
|
internal SkinnableLighting Lighting { get; private set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; } = null!;
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
private bool positionTransferred;
|
private Vector2 screenSpacePosition;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
@ -32,37 +36,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Apply(JudgementResult result, DrawableHitObject? judgedObject)
|
||||||
|
{
|
||||||
|
base.Apply(result, judgedObject);
|
||||||
|
|
||||||
|
if (judgedObject is not DrawableOsuHitObject osuObject)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AccentColour = osuObject.AccentColour.Value;
|
||||||
|
|
||||||
|
switch (osuObject)
|
||||||
|
{
|
||||||
|
case DrawableSlider slider:
|
||||||
|
screenSpacePosition = slider.TailCircle.ToScreenSpace(slider.TailCircle.OriginPosition);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
screenSpacePosition = osuObject.ToScreenSpace(osuObject.OriginPosition);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Scale = new Vector2(osuObject.HitObject.Scale);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void PrepareForUse()
|
protected override void PrepareForUse()
|
||||||
{
|
{
|
||||||
base.PrepareForUse();
|
base.PrepareForUse();
|
||||||
|
|
||||||
Lighting.ResetAnimation();
|
Lighting.ResetAnimation();
|
||||||
Lighting.SetColourFrom(JudgedObject, Result);
|
Lighting.SetColourFrom(this, Result);
|
||||||
|
Position = Parent!.ToLocalSpace(screenSpacePosition);
|
||||||
positionTransferred = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
|
|
||||||
{
|
|
||||||
switch (osuObject)
|
|
||||||
{
|
|
||||||
case DrawableSlider slider:
|
|
||||||
Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
positionTransferred = true;
|
|
||||||
|
|
||||||
Scale = new Vector2(osuObject.HitObject.Scale);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ApplyHitAnimations()
|
protected override void ApplyHitAnimations()
|
||||||
|
@ -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.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -12,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
internal partial class SkinnableLighting : SkinnableSprite
|
internal partial class SkinnableLighting : SkinnableSprite
|
||||||
{
|
{
|
||||||
private DrawableHitObject targetObject;
|
private DrawableOsuJudgement? targetJudgement;
|
||||||
private JudgementResult targetResult;
|
private JudgementResult? targetResult;
|
||||||
|
|
||||||
public SkinnableLighting()
|
public SkinnableLighting()
|
||||||
: base("lighting")
|
: base("lighting")
|
||||||
@ -29,11 +27,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the lighting colour from a given hitobject and result.
|
/// Updates the lighting colour from a given hitobject and result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="targetObject">The <see cref="DrawableHitObject"/> that's been judged.</param>
|
/// <param name="targetJudgement">The <see cref="DrawableHitObject"/> that's been judged.</param>
|
||||||
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetObject"/> was judged with.</param>
|
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetJudgement"/> was judged with.</param>
|
||||||
public void SetColourFrom(DrawableHitObject targetObject, JudgementResult targetResult)
|
public void SetColourFrom(DrawableOsuJudgement targetJudgement, JudgementResult? targetResult)
|
||||||
{
|
{
|
||||||
this.targetObject = targetObject;
|
this.targetJudgement = targetJudgement;
|
||||||
this.targetResult = targetResult;
|
this.targetResult = targetResult;
|
||||||
|
|
||||||
updateColour();
|
updateColour();
|
||||||
@ -41,10 +39,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
private void updateColour()
|
private void updateColour()
|
||||||
{
|
{
|
||||||
if (targetObject == null || targetResult == null)
|
if (targetJudgement == null || targetResult == null)
|
||||||
Colour = Color4.White;
|
Colour = Color4.White;
|
||||||
else
|
else
|
||||||
Colour = targetResult.IsHit ? targetObject.AccentColour.Value : Color4.Transparent;
|
Colour = targetResult.IsHit ? targetJudgement.AccentColour : Color4.Transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects
|
namespace osu.Game.Rulesets.Osu.Objects
|
||||||
{
|
{
|
||||||
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition
|
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition, IHasTimePreempt
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The radius of hit objects (ie. the radius of a <see cref="HitCircle"/>).
|
/// The radius of hit objects (ie. the radius of a <see cref="HitCircle"/>).
|
||||||
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const double PREEMPT_MAX = 1800;
|
public const double PREEMPT_MAX = 1800;
|
||||||
|
|
||||||
public double TimePreempt = 600;
|
public double TimePreempt { get; set; } = 600;
|
||||||
public double TimeFadeIn = 400;
|
public double TimeFadeIn = 400;
|
||||||
|
|
||||||
private HitObjectProperty<Vector2> position;
|
private HitObjectProperty<Vector2> position;
|
||||||
|
@ -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)
|
||||||
|
@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types;
|
|||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Layout;
|
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -39,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
|
||||||
@ -63,8 +67,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
// -1 signals that the part is unusable, and should not be drawn
|
// -1 signals that the part is unusable, and should not be drawn
|
||||||
parts[i].InvalidationID = -1;
|
parts[i].InvalidationID = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
AddLayout(partSizeCache);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -95,12 +97,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly LayoutValue<Vector2> partSizeCache = new LayoutValue<Vector2>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
|
|
||||||
|
|
||||||
private Vector2 partSize => partSizeCache.IsValid
|
|
||||||
? partSizeCache.Value
|
|
||||||
: (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of time to fade the cursor trail pieces.
|
/// The amount of time to fade the cursor trail pieces.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -156,6 +152,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
protected void AddTrail(Vector2 position)
|
protected void AddTrail(Vector2 position)
|
||||||
{
|
{
|
||||||
|
position = ToLocalSpace(position);
|
||||||
|
|
||||||
if (InterpolateMovements)
|
if (InterpolateMovements)
|
||||||
{
|
{
|
||||||
if (!lastPosition.HasValue)
|
if (!lastPosition.HasValue)
|
||||||
@ -174,7 +172,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
float distance = diff.Length;
|
float distance = diff.Length;
|
||||||
Vector2 direction = diff / distance;
|
Vector2 direction = diff / distance;
|
||||||
|
|
||||||
float interval = partSize.X / 2.5f * IntervalMultiplier;
|
float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
|
||||||
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
|
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
|
||||||
|
|
||||||
for (float d = interval; d < stopAt; d += interval)
|
for (float d = interval; d < stopAt; d += interval)
|
||||||
@ -191,10 +189,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPart(Vector2 screenSpacePosition)
|
private void addPart(Vector2 localSpacePosition)
|
||||||
{
|
{
|
||||||
parts[currentIndex].Position = screenSpacePosition;
|
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;
|
||||||
@ -206,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +220,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
private float fadeExponent;
|
private float fadeExponent;
|
||||||
|
|
||||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||||
private Vector2 size;
|
|
||||||
private Vector2 originPosition;
|
private Vector2 originPosition;
|
||||||
|
|
||||||
private IVertexBatch<TexturedTrailVertex> vertexBatch;
|
private IVertexBatch<TexturedTrailVertex> vertexBatch;
|
||||||
@ -236,7 +235,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
shader = Source.shader;
|
shader = Source.shader;
|
||||||
texture = Source.texture;
|
texture = Source.texture;
|
||||||
size = Source.partSize;
|
|
||||||
time = Source.time;
|
time = Source.time;
|
||||||
fadeExponent = Source.FadeExponent;
|
fadeExponent = Source.FadeExponent;
|
||||||
|
|
||||||
@ -277,6 +275,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
RectangleF textureRect = texture.GetTextureRect();
|
RectangleF textureRect = texture.GetTextureRect();
|
||||||
|
|
||||||
|
renderer.PushLocalMatrix(DrawInfo.Matrix);
|
||||||
|
|
||||||
foreach (var part in parts)
|
foreach (var part in parts)
|
||||||
{
|
{
|
||||||
if (part.InvalidationID == -1)
|
if (part.InvalidationID == -1)
|
||||||
@ -287,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (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,
|
||||||
@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (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,
|
||||||
@ -305,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * 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,
|
||||||
@ -314,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * 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,
|
||||||
@ -322,6 +322,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderer.PopLocalMatrix();
|
||||||
|
|
||||||
vertexBatch.Draw();
|
vertexBatch.Draw();
|
||||||
shader.Unbind();
|
shader.Unbind();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user