1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 06:22:56 +08:00

Merge branch 'master' into hold-note-tail-topcentre

This commit is contained in:
Long Nguyen 2024-09-12 11:29:01 +07:00 committed by GitHub
commit 1bf5ab4408
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
659 changed files with 21816 additions and 6679 deletions

View File

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

View File

@ -64,10 +64,11 @@ 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: 60 timeout-minutes: 120
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -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

View File

@ -111,7 +111,7 @@ jobs:
steps: steps:
- name: Check permissions - name: Check permissions
run: | run: |
ALLOWED_USERS=(smoogipoo peppy bdach) ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte)
for i in "${ALLOWED_USERS[@]}"; do for i in "${ALLOWED_USERS[@]}"; do
if [[ "${{ github.actor }}" == "$i" ]]; then if [[ "${{ github.actor }}" == "$i" ]]; then
exit 0 exit 0

2
.gitignore vendored
View File

@ -265,6 +265,8 @@ __pycache__/
.idea/**/usage.statistics.xml .idea/**/usage.statistics.xml
.idea/**/dictionaries .idea/**/dictionaries
.idea/**/shelf .idea/**/shelf
.idea/*/.idea/projectSettingsUpdater.xml
.idea/*/.idea/encodings.xml
# Generated files # Generated files
.idea/**/contentModel.xml .idea/**/contentModel.xml

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -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.

View File

@ -7,7 +7,6 @@ T:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> ins
M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods. T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead. M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty. M:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead. M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.

View File

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

View File

@ -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();

View File

@ -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()

View File

@ -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();
});
} }
} }
} }

View File

@ -2,7 +2,6 @@
// 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.Security.Principal;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -21,48 +20,14 @@ namespace osu.Desktop.Security
[Resolved] [Resolved]
private INotificationOverlay notifications { get; set; } = null!; private INotificationOverlay notifications { get; set; } = null!;
private bool elevated;
[BackgroundDependencyLoader]
private void load()
{
elevated = checkElevated();
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
if (elevated) if (Environment.IsPrivilegedProcess)
notifications.Post(new ElevatedPrivilegesNotification()); notifications.Post(new ElevatedPrivilegesNotification());
} }
private bool checkElevated()
{
try
{
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
if (!OperatingSystem.IsWindows()) return false;
var windowsIdentity = WindowsIdentity.GetCurrent();
var windowsPrincipal = new WindowsPrincipal(windowsIdentity);
return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
case RuntimeInfo.Platform.macOS:
case RuntimeInfo.Platform.Linux:
return Mono.Unix.Native.Syscall.geteuid() == 0;
}
}
catch
{
}
return false;
}
private partial class ElevatedPrivilegesNotification : SimpleNotification private partial class ElevatedPrivilegesNotification : SimpleNotification
{ {
public override bool IsImportant => true; public override bool IsImportant => true;

View File

@ -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()
{
}
}
}
}

View 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;
}
}
}

View File

@ -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,10 +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="Mono.Posix.NETStandard" Version="1.0.0" />
<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" />

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public partial class TestSceneCatchEditorSaving : EditorSavingTestScene
{
protected override Ruleset CreateRuleset() => new CatchRuleset();
[Test]
public void TestCatchJuiceStreamTickCorrect()
{
AddStep("enter timing mode", () => InputManager.Key(Key.F3));
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("enter compose mode", () => InputManager.Key(Key.F1));
Vector2 startPoint = Vector2.Zero;
float increment = 0;
AddUntilStep("wait for playfield", () => this.ChildrenOfType<CatchPlayfield>().FirstOrDefault()?.IsLoaded == true);
AddStep("move to centre", () =>
{
var playfield = this.ChildrenOfType<CatchPlayfield>().Single();
startPoint = playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Height / 3);
increment = playfield.ScreenSpaceDrawQuad.Height / 10;
InputManager.MoveMouseTo(startPoint);
});
AddStep("choose juice stream placing tool", () => InputManager.Key(Key.Number3));
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(2 * increment, -increment)));
AddStep("add node", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(-2 * increment, -2 * increment)));
AddStep("add node", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(0, -3 * increment)));
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
AddUntilStep("juice stream placed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1));
int largeDropletCount = 0, tinyDropletCount = 0;
AddStep("store droplet count", () =>
{
largeDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet));
tinyDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet));
});
SaveEditor();
ReloadEditorToSameBeatmap();
AddAssert("large droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)), () => Is.EqualTo(largeDropletCount));
AddAssert("tiny droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)), () => Is.EqualTo(tinyDropletCount));
}
}
}

View File

@ -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);
}); });
} }

View File

@ -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);
});
} }
} }
} }

View File

@ -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 System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests
{
public partial class TestSceneCatchReplayHandling : OsuManualInputManagerTestScene
{
[Test]
public void TestReplayDetach()
{
DrawableCatchRuleset drawableRuleset = null!;
float catcherPosition = 0;
AddStep("create drawable ruleset", () => Child = drawableRuleset = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), []));
AddStep("attach replay", () => drawableRuleset.SetReplayScore(new Score()));
AddStep("store catcher position", () => catcherPosition = drawableRuleset.ChildrenOfType<Catcher>().Single().X);
AddStep("hold down left", () => InputManager.PressKey(Key.Left));
AddAssert("catcher didn't move", () => drawableRuleset.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(catcherPosition));
AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
AddStep("detach replay", () => drawableRuleset.SetReplayScore(null));
AddStep("hold down left", () => InputManager.PressKey(Key.Left));
AddUntilStep("catcher moved", () => drawableRuleset.ChildrenOfType<Catcher>().Single().X, () => Is.Not.EqualTo(catcherPosition));
AddStep("release left", () => InputManager.ReleaseKey(Key.Left));
}
}
}

View File

@ -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);

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y, LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration. // this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1 : ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity, TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1 SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield(); }.Yield();

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -29,6 +28,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -62,43 +62,43 @@ namespace osu.Game.Rulesets.Catch
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new CatchModNightcore(); yield return new CatchModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new CatchModDoubleTime(); yield return new CatchModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new CatchModPerfect(); yield return new CatchModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new CatchModSuddenDeath(); yield return new CatchModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new CatchModCinema(); yield return new CatchModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new CatchModAutoplay(); yield return new CatchModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new CatchModEasy(); yield return new CatchModEasy();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new CatchModFlashlight(); yield return new CatchModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new CatchModHalfTime(); yield return new CatchModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new CatchModHardRock(); yield return new CatchModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new CatchModHidden(); yield return new CatchModHidden();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new CatchModNoFail(); yield return new CatchModNoFail();
if (mods.HasFlagFast(LegacyMods.Relax)) if (mods.HasFlag(LegacyMods.Relax))
yield return new CatchModRelax(); yield return new CatchModRelax();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }
@ -223,6 +223,12 @@ namespace osu.Game.Rulesets.Catch
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
new DifficultySection(),
new ColoursSection(),
];
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
@ -248,5 +254,7 @@ namespace osu.Game.Rulesets.Catch
return adjustedDifficulty; return adjustedDifficulty;
} }
public override bool EditorShowScrollSpeed => false;
} }
} }

View File

@ -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)

View File

@ -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)

View File

@ -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();
} }
} }
} }

View File

@ -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;
} }
} }

View File

@ -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)
}; };
} }

View File

@ -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;
} }

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -121,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Edit
result.ScreenSpacePosition.X = screenSpacePosition.X; result.ScreenSpacePosition.X = screenSpacePosition.X;
if (snapType.HasFlagFast(SnapType.RelativeGrids)) if (snapType.HasFlag(SnapType.RelativeGrids))
{ {
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius) Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)

View File

@ -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;

View File

@ -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;
}
} }
} }

View File

@ -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.");
} }
} }

View File

@ -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);
} }

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
@ -46,16 +47,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TickDistanceMultiplier = 1; public double TickDistanceMultiplier = 1;
[JsonIgnore] [JsonIgnore]
private double velocityFactor; public double Velocity { get; private set; }
[JsonIgnore] [JsonIgnore]
private double tickDistanceFactor; public double TickDistance { get; private set; }
[JsonIgnore]
public double Velocity => velocityFactor * SliderVelocityMultiplier;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * TickDistanceMultiplier;
/// <summary> /// <summary>
/// The length of one span of this <see cref="JuiceStream"/>. /// The length of one span of this <see cref="JuiceStream"/>.
@ -68,8 +63,13 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength; Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME);
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
double scoringDistance = Velocity * timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
} }
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.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);
} }

View File

@ -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);
} }

View File

@ -1,13 +1,18 @@
// 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.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
@ -30,5 +35,43 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull(); var config = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(ManiaRulesetSetting.ScrollDirection, direction); config.BindWith(ManiaRulesetSetting.ScrollDirection, direction);
} }
[Test]
public void TestReloadOnBPMChange()
{
HitObjectComposer oldComposer = null!;
AddStep("store composer", () => oldComposer = this.ChildrenOfType<HitObjectComposer>().Single());
AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
AddStep("switch to timing tab", () => InputManager.Key(Key.F3));
AddUntilStep("wait for loaded", () => this.ChildrenOfType<TimingAdjustButton>().ElementAtOrDefault(1), () => Is.Not.Null);
AddStep("change timing point BPM", () =>
{
var bpmControl = this.ChildrenOfType<TimingAdjustButton>().ElementAt(1);
InputManager.MoveMouseTo(bpmControl);
InputManager.Click(MouseButton.Left);
});
AddStep("switch back to composer", () => InputManager.Key(Key.F1));
AddUntilStep("composer reloaded", () =>
{
var composer = this.ChildrenOfType<HitObjectComposer>().SingleOrDefault();
return composer != null && composer != oldComposer;
});
AddStep("store composer", () => oldComposer = this.ChildrenOfType<HitObjectComposer>().Single());
AddUntilStep("composer stored", () => oldComposer, () => Is.Not.Null);
AddStep("undo", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Z);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("composer reloaded", () =>
{
var composer = this.ChildrenOfType<HitObjectComposer>().SingleOrDefault();
return composer != null && composer != oldComposer;
});
}
} }
} }

View File

@ -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));

View File

@ -2,6 +2,8 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -17,5 +19,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Mod = new ManiaModInvert(), Mod = new ManiaModInvert(),
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
}); });
[Test]
public void TestBreaksPreservedOnOriginalBeatmap()
{
var beatmap = CreateBeatmap(new ManiaRuleset().RulesetInfo);
beatmap.Breaks.Clear();
beatmap.Breaks.Add(new BreakPeriod(0, 1000));
var workingBeatmap = new FlatWorkingBeatmap(beatmap);
var playableWithInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo, new[] { new ManiaModInvert() });
Assert.That(playableWithInvert.Breaks.Count, Is.Zero);
var playableWithoutInvert = workingBeatmap.GetPlayableBeatmap(new ManiaRuleset().RulesetInfo);
Assert.That(playableWithoutInvert.Breaks.Count, Is.Not.Zero);
Assert.That(playableWithoutInvert.Breaks[0], Is.EqualTo(new BreakPeriod(0, 1000)));
}
} }
} }

View File

@ -0,0 +1,643 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public partial class TestSceneManiaModNoRelease : RateAdjustedBeatmapTestScene
{
private const double time_before_head = 250;
private const double time_head = 1500;
private const double time_during_hold_1 = 2500;
private const double time_tail = 4000;
private const double time_after_tail = 5250;
private List<JudgementResult> judgementResults = new List<JudgementResult>();
/// <summary>
/// -----[ ]-----
/// o o
/// </summary>
[Test]
public void TestNoInput()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreMiss);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestCorrectInput()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Perfect);
assertNoteJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestLateRelease()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Perfect);
assertNoteJudgement(HitResult.IgnoreHit);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestPressTooEarlyAndReleaseAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail, ManiaAction.Key1),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestPressTooEarlyAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
}
/// <summary>
/// -----[ ]-----
/// xo x o
/// </summary>
[Test]
public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
new ManiaReplayFrame(time_before_head + 10),
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Perfect);
}
/// <summary>
/// -----[ ]-----
/// xo x o
/// </summary>
[Test]
public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_before_head, ManiaAction.Key1),
new ManiaReplayFrame(time_before_head + 10),
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Perfect);
}
/// <summary>
/// -----[ ]-----
/// xo o
/// </summary>
[Test]
public void TestPressAtStartAndBreak()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 10),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Miss);
}
/// <summary>
/// -----[ ]-----
/// xox o
/// </summary>
[Test]
public void TestPressAtStartThenReleaseAndImmediatelyRepress()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 1),
new ManiaReplayFrame(time_head + 2, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertComboAtJudgement(0, 1);
assertTailJudgement(HitResult.Meh);
assertComboAtJudgement(1, 0);
assertComboAtJudgement(3, 1);
}
/// <summary>
/// -----[ ]-----
/// xo x o
/// </summary>
[Test]
public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 10),
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]-----
/// xo x o o
/// </summary>
[Test]
public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 10),
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestPressDuringNoteAndReleaseAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
new ManiaReplayFrame(time_after_tail),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]-----
/// x o o
/// </summary>
[Test]
public void TestPressDuringNoteAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]--------------
/// xo
/// </summary>
[Test]
public void TestPressAndReleaseAfterTailWithCloseByHead()
{
const int duration = 30;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
// hold note is very short, to make the head still in range
new HoldNote
{
StartTime = time_head,
Duration = duration,
Column = 0,
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head + duration + 60, ManiaAction.Key1),
new ManiaReplayFrame(time_head + duration + 70),
}, beatmap);
assertHeadJudgement(HitResult.Ok);
assertTailJudgement(HitResult.Perfect);
}
/// <summary>
/// -----[ ]-O-------------
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustBeforeTailWithNearbyNoteAndCloseByHead()
{
Note note;
const int duration = 50;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
// hold note is very short, to make the head still in range
new HoldNote
{
StartTime = time_head,
Duration = duration,
Column = 0,
},
{
// Next note within tail lenience
note = new Note
{
StartTime = time_head + duration + 10
}
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head + duration, ManiaAction.Key1),
new ManiaReplayFrame(time_head + duration + 10),
}, beatmap);
assertHeadJudgement(HitResult.Good);
assertTailJudgement(HitResult.Perfect);
assertHitObjectJudgement(note, HitResult.Miss);
}
/// <summary>
/// -----[ ]--O--
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustBeforeTailWithNearbyNote()
{
Note note;
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
},
{
// Next note within tail lenience
note = new Note
{
StartTime = time_tail + 50
}
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail - 10, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
}, beatmap);
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Good);
}
/// <summary>
/// -----[ ]-----
/// xo
/// </summary>
[Test]
public void TestPressAndReleaseJustAfterTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail + 20, ManiaAction.Key1),
new ManiaReplayFrame(time_tail + 30),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Meh);
}
/// <summary>
/// -----[ ]--O--
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseJustAfterTailWithNearbyNote()
{
// Next note within tail lenience
Note note = new Note { StartTime = time_tail + 50 };
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
},
note
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail + 10, ManiaAction.Key1),
new ManiaReplayFrame(time_tail + 20),
}, beatmap);
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Great);
}
/// <summary>
/// -----[ ]-----
/// xo o
/// </summary>
[Test]
public void TestPressAndReleaseAtTail()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_tail, ManiaAction.Key1),
new ManiaReplayFrame(time_tail + 10),
});
assertHeadJudgement(HitResult.Miss);
assertTailJudgement(HitResult.Meh);
}
[Test]
public void TestMissReleaseAndHitSecondRelease()
{
var windows = new ManiaHitWindows();
windows.SetDifficulty(10);
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = 1000,
Duration = 500,
Column = 0,
},
new HoldNote
{
StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10,
Duration = 500,
Column = 0,
},
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty
{
SliderTickRate = 4,
OverallDifficulty = 10,
},
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1),
new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()),
}, beatmap);
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => !j.Type.IsHit()));
AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type.IsHit()));
}
[Test]
public void TestZeroLength()
{
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = 1000,
Duration = 0,
Column = 0,
},
},
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1),
new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1),
}, beatmap);
AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type.IsHit()));
}
private void assertHitObjectJudgement(HitObject hitObject, HitResult result)
=> AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result));
private void assertHeadJudgement(HitResult result)
=> AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type, () => Is.EqualTo(result));
private void assertTailJudgement(HitResult result)
=> AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type, () => Is.EqualTo(result));
private void assertNoteJudgement(HitResult result)
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
private void assertComboAtJudgement(int judgementIndex, int combo)
=> AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo));
private ScoreAccessibleReplayPlayer currentPlayer = null!;
private void performTest(List<ReplayFrame> frames, Beatmap<ManiaHitObject>? beatmap = null)
{
if (beatmap == null)
{
beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo,
},
};
beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
}
AddStep("load player", () =>
{
SelectedMods.Value = new List<Mod>
{
new ManiaModNoRelease()
};
Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}

View File

@ -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;
});
}
}
}

View File

@ -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()`).

View File

@ -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()

View File

@ -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)
}; };
}); });
} }

View File

@ -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]

View File

@ -474,8 +474,8 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => !j.Type.IsHit())); .All(j => !j.Type.IsHit()));
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) AddAssert("second hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type.IsHit())); .All(j => j.Type.IsHit()));
} }
[Test] [Test]

View File

@ -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);
}
}
}

View File

@ -8,7 +8,6 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests
} }
private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor) private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor)
=> hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor); => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor);
private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor) private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor)
=> verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor)); => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor));

View File

@ -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)

View File

@ -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>()
};
} }
} }
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osuTK; using osuTK;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
else else
convertType |= PatternType.LowProbability; convertType |= PatternType.LowProbability;
if (!convertType.HasFlagFast(PatternType.KeepSingle)) if (!convertType.HasFlag(PatternType.KeepSingle))
{ {
if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8) if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8)
convertType |= PatternType.Mirror; convertType |= PatternType.Mirror;
@ -102,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0;
if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any())
{ {
// Generate a new pattern by copying the last hit objects in reverse-column order // Generate a new pattern by copying the last hit objects in reverse-column order
for (int i = RandomStart; i < TotalColumns; i++) for (int i = RandomStart; i < TotalColumns; i++)
@ -114,7 +113,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern; return pattern;
} }
if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
// If we convert to 7K + 1, let's not overload the special key // If we convert to 7K + 1, let's not overload the special key
&& (TotalColumns != 8 || lastColumn != 0) && (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column // Make sure the last column was not the centre column
@ -127,7 +126,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern; return pattern;
} }
if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any())
{ {
// Generate a new pattern by placing on the already filled columns // Generate a new pattern by placing on the already filled columns
for (int i = RandomStart; i < TotalColumns; i++) for (int i = RandomStart; i < TotalColumns; i++)
@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (PreviousPattern.HitObjects.Count() == 1) if (PreviousPattern.HitObjects.Count() == 1)
{ {
if (convertType.HasFlagFast(PatternType.Stair)) if (convertType.HasFlag(PatternType.Stair))
{ {
// Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next"
int targetColumn = lastColumn + 1; int targetColumn = lastColumn + 1;
@ -152,7 +151,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern; return pattern;
} }
if (convertType.HasFlagFast(PatternType.ReverseStair)) if (convertType.HasFlag(PatternType.ReverseStair))
{ {
// Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous"
int targetColumn = lastColumn - 1; int targetColumn = lastColumn - 1;
@ -164,10 +163,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
} }
} }
if (convertType.HasFlagFast(PatternType.KeepSingle)) if (convertType.HasFlag(PatternType.KeepSingle))
return generateRandomNotes(1); return generateRandomNotes(1);
if (convertType.HasFlagFast(PatternType.Mirror)) if (convertType.HasFlag(PatternType.Mirror))
{ {
if (ConversionDifficulty > 6.5) if (ConversionDifficulty > 6.5)
return generateRandomPatternWithMirrored(0.12, 0.38, 0.12); return generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
@ -179,7 +178,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 6.5) if (ConversionDifficulty > 6.5)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.78, 0.42, 0, 0); return generateRandomPattern(0.78, 0.42, 0, 0);
return generateRandomPattern(1, 0.62, 0, 0); return generateRandomPattern(1, 0.62, 0, 0);
@ -187,7 +186,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 4) if (ConversionDifficulty > 4)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.35, 0.08, 0, 0); return generateRandomPattern(0.35, 0.08, 0, 0);
return generateRandomPattern(0.52, 0.15, 0, 0); return generateRandomPattern(0.52, 0.15, 0, 0);
@ -195,7 +194,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 2) if (ConversionDifficulty > 2)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateRandomPattern(0.18, 0, 0, 0); return generateRandomPattern(0.18, 0, 0, 0);
return generateRandomPattern(0.45, 0, 0, 0); return generateRandomPattern(0.45, 0, 0, 0);
@ -208,9 +207,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in p.HitObjects) foreach (var obj in p.HitObjects)
{ {
if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1) if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1)
StairType = PatternType.ReverseStair; StairType = PatternType.ReverseStair;
if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart) if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart)
StairType = PatternType.Stair; StairType = PatternType.Stair;
} }
@ -230,7 +229,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
var pattern = new Pattern(); var pattern = new Pattern();
bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack); bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack);
if (!allowStacking) if (!allowStacking)
noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects); noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects);
@ -250,7 +249,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int getNextColumn(int last) int getNextColumn(int last)
{ {
if (convertType.HasFlagFast(PatternType.Gathered)) if (convertType.HasFlag(PatternType.Gathered))
{ {
last++; last++;
if (last == TotalColumns) if (last == TotalColumns)
@ -297,7 +296,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
{ {
if (convertType.HasFlagFast(PatternType.ForceNotStack)) if (convertType.HasFlag(PatternType.ForceNotStack))
return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
var pattern = new Pattern(); var pattern = new Pattern();

View File

@ -7,7 +7,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 6.5) if (ConversionDifficulty > 6.5)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.78, 0.3, 0); return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03); return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
@ -147,7 +146,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 4) if (ConversionDifficulty > 4)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.43, 0.08, 0); return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(StartTime, 0.56, 0.18, 0); return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
@ -155,13 +154,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (ConversionDifficulty > 2.5) if (ConversionDifficulty > 2.5)
{ {
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.3, 0, 0); return generateNRandomNotes(StartTime, 0.3, 0, 0);
return generateNRandomNotes(StartTime, 0.37, 0.08, 0); return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
} }
if (convertType.HasFlagFast(PatternType.LowProbability)) if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(StartTime, 0.17, 0, 0); return generateNRandomNotes(StartTime, 0.17, 0, 0);
return generateNRandomNotes(StartTime, 0.27, 0, 0); return generateNRandomNotes(StartTime, 0.27, 0, 0);
@ -219,7 +218,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern(); var pattern = new Pattern();
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
int lastColumn = nextColumn; int lastColumn = nextColumn;
@ -371,7 +370,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability); bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes) if (canGenerateTwoNotes)
@ -404,7 +403,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int endTime = startTime + SegmentDuration * SpanCount; int endTime = startTime + SegmentDuration * SpanCount;
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
for (int i = 0; i < columnRepeat; i++) for (int i = 0; i < columnRepeat; i++)
@ -433,7 +432,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern(); var pattern = new Pattern();
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
holdColumn = FindAvailableColumn(holdColumn, PreviousPattern); holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
// Create the hold note // Create the hold note

View File

@ -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);
} }

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
public BindableBool ShowSpeedChanges { get; } = new BindableBool(); public BindableBool ShowSpeedChanges { get; } = new BindableBool();
public double? TimelineTimeRange { get; set; }
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo; public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods) public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)
@ -38,5 +41,11 @@ namespace osu.Game.Rulesets.Mania.Edit
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = Vector2.One Size = Vector2.One
}; };
protected override void Update()
{
TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<int>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
base.Update();
}
} }
} }

View File

@ -1,11 +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.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
@ -14,6 +13,7 @@ using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -21,7 +21,10 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject> public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
{ {
private DrawableManiaEditorRuleset drawableRuleset; private DrawableManiaEditorRuleset drawableRuleset = null!;
[Resolved]
private EditorScreenWithTimeline? screenWithTimeline { get; set; }
public ManiaHitObjectComposer(Ruleset ruleset) public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset) : base(ruleset)
@ -72,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Edit
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column)) if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
continue; continue;
ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column); ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
if (current == null) if (current == null)
continue; continue;
@ -83,5 +86,13 @@ namespace osu.Game.Rulesets.Mania.Edit
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
} }
} }
protected override void Update()
{
base.Update();
if (screenWithTimeline?.TimelineArea.Timeline != null)
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2;
}
} }
} }

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!; private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
private LabelledSwitchButton specialStyle { get; set; } = null!;
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!; private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!; private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!; private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
@ -49,6 +50,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
Precision = 1, Precision = 1,
} }
}, },
specialStyle = new LabelledSwitchButton
{
Label = "Use special (N+1) style",
FixedLabelWidth = LABEL_WIDTH,
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
},
healthDrainSlider = new LabelledSliderBar<float> healthDrainSlider = new LabelledSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsDrain, Label = BeatmapsetsStrings.ShowStatsDrain,
@ -145,6 +153,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
// for now, update these on commit rather than making BeatmapMetadata bindables. // for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction. // after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value; Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;

View File

@ -1,49 +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.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Mania.Edit.Setup
{
public partial class ManiaSetupSection : RulesetSetupSection
{
private LabelledSwitchButton specialStyle;
public ManiaSetupSection()
: base(new ManiaRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
specialStyle = new LabelledSwitchButton
{
Label = "Use special (N+1) style",
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
specialStyle.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
Beatmap.SaveState();
}
}
}

View File

@ -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,

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
@ -89,79 +88,79 @@ namespace osu.Game.Rulesets.Mania
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{ {
if (mods.HasFlagFast(LegacyMods.Nightcore)) if (mods.HasFlag(LegacyMods.Nightcore))
yield return new ManiaModNightcore(); yield return new ManiaModNightcore();
else if (mods.HasFlagFast(LegacyMods.DoubleTime)) else if (mods.HasFlag(LegacyMods.DoubleTime))
yield return new ManiaModDoubleTime(); yield return new ManiaModDoubleTime();
if (mods.HasFlagFast(LegacyMods.Perfect)) if (mods.HasFlag(LegacyMods.Perfect))
yield return new ManiaModPerfect(); yield return new ManiaModPerfect();
else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) else if (mods.HasFlag(LegacyMods.SuddenDeath))
yield return new ManiaModSuddenDeath(); yield return new ManiaModSuddenDeath();
if (mods.HasFlagFast(LegacyMods.Cinema)) if (mods.HasFlag(LegacyMods.Cinema))
yield return new ManiaModCinema(); yield return new ManiaModCinema();
else if (mods.HasFlagFast(LegacyMods.Autoplay)) else if (mods.HasFlag(LegacyMods.Autoplay))
yield return new ManiaModAutoplay(); yield return new ManiaModAutoplay();
if (mods.HasFlagFast(LegacyMods.Easy)) if (mods.HasFlag(LegacyMods.Easy))
yield return new ManiaModEasy(); yield return new ManiaModEasy();
if (mods.HasFlagFast(LegacyMods.FadeIn)) if (mods.HasFlag(LegacyMods.FadeIn))
yield return new ManiaModFadeIn(); yield return new ManiaModFadeIn();
if (mods.HasFlagFast(LegacyMods.Flashlight)) if (mods.HasFlag(LegacyMods.Flashlight))
yield return new ManiaModFlashlight(); yield return new ManiaModFlashlight();
if (mods.HasFlagFast(LegacyMods.HalfTime)) if (mods.HasFlag(LegacyMods.HalfTime))
yield return new ManiaModHalfTime(); yield return new ManiaModHalfTime();
if (mods.HasFlagFast(LegacyMods.HardRock)) if (mods.HasFlag(LegacyMods.HardRock))
yield return new ManiaModHardRock(); yield return new ManiaModHardRock();
if (mods.HasFlagFast(LegacyMods.Hidden)) if (mods.HasFlag(LegacyMods.Hidden))
yield return new ManiaModHidden(); yield return new ManiaModHidden();
if (mods.HasFlagFast(LegacyMods.Key1)) if (mods.HasFlag(LegacyMods.Key1))
yield return new ManiaModKey1(); yield return new ManiaModKey1();
if (mods.HasFlagFast(LegacyMods.Key2)) if (mods.HasFlag(LegacyMods.Key2))
yield return new ManiaModKey2(); yield return new ManiaModKey2();
if (mods.HasFlagFast(LegacyMods.Key3)) if (mods.HasFlag(LegacyMods.Key3))
yield return new ManiaModKey3(); yield return new ManiaModKey3();
if (mods.HasFlagFast(LegacyMods.Key4)) if (mods.HasFlag(LegacyMods.Key4))
yield return new ManiaModKey4(); yield return new ManiaModKey4();
if (mods.HasFlagFast(LegacyMods.Key5)) if (mods.HasFlag(LegacyMods.Key5))
yield return new ManiaModKey5(); yield return new ManiaModKey5();
if (mods.HasFlagFast(LegacyMods.Key6)) if (mods.HasFlag(LegacyMods.Key6))
yield return new ManiaModKey6(); yield return new ManiaModKey6();
if (mods.HasFlagFast(LegacyMods.Key7)) if (mods.HasFlag(LegacyMods.Key7))
yield return new ManiaModKey7(); yield return new ManiaModKey7();
if (mods.HasFlagFast(LegacyMods.Key8)) if (mods.HasFlag(LegacyMods.Key8))
yield return new ManiaModKey8(); yield return new ManiaModKey8();
if (mods.HasFlagFast(LegacyMods.Key9)) if (mods.HasFlag(LegacyMods.Key9))
yield return new ManiaModKey9(); yield return new ManiaModKey9();
if (mods.HasFlagFast(LegacyMods.KeyCoop)) if (mods.HasFlag(LegacyMods.KeyCoop))
yield return new ManiaModDualStages(); yield return new ManiaModDualStages();
if (mods.HasFlagFast(LegacyMods.NoFail)) if (mods.HasFlag(LegacyMods.NoFail))
yield return new ManiaModNoFail(); yield return new ManiaModNoFail();
if (mods.HasFlagFast(LegacyMods.Random)) if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom(); yield return new ManiaModRandom();
if (mods.HasFlagFast(LegacyMods.Mirror)) if (mods.HasFlag(LegacyMods.Mirror))
yield return new ManiaModMirror(); yield return new ManiaModMirror();
if (mods.HasFlagFast(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ModScoreV2();
} }
@ -241,6 +240,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModEasy(), new ManiaModEasy(),
new ManiaModNoFail(), new ManiaModNoFail(),
new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()), new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()),
new ManiaModNoRelease(),
}; };
case ModType.DifficultyIncrease: case ModType.DifficultyIncrease:
@ -419,9 +419,10 @@ namespace osu.Game.Rulesets.Mania
return new ManiaFilterCriteria(); return new ManiaFilterCriteria();
} }
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection(); public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
[
public override SetupSection CreateEditorDifficultySection() => new ManiaDifficultySection(); new ManiaDifficultySection(),
];
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null) public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods); => ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);

View File

@ -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"/>.

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) }; public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert), typeof(ManiaModNoRelease) };
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {

View File

@ -0,0 +1,110 @@
// 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.Linq;
using System.Threading;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
public partial class ManiaModNoRelease : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableRuleset<ManiaHitObject>
{
public override string Name => "No Release";
public override string Acronym => "NR";
public override LocalisableString Description => "No more timing the end of hold notes.";
public override double ScoreMultiplier => 0.9;
public override ModType Type => ModType.DifficultyReduction;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
var hitObjects = maniaBeatmap.HitObjects.Select(obj =>
{
if (obj is HoldNote hold)
return new NoReleaseHoldNote(hold);
return obj;
}).ToList();
maniaBeatmap.HitObjects = hitObjects;
}
public void ApplyToDrawableRuleset(DrawableRuleset<ManiaHitObject> drawableRuleset)
{
var maniaRuleset = (DrawableManiaRuleset)drawableRuleset;
foreach (var stage in maniaRuleset.Playfield.Stages)
{
foreach (var column in stage.Columns)
{
column.RegisterPool<NoReleaseTailNote, NoReleaseDrawableHoldNoteTail>(10, 50);
}
}
}
private partial class NoReleaseDrawableHoldNoteTail : DrawableHoldNoteTail
{
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// apply perfect once the tail is reached
if (HoldNote.HoldStartTime != null && timeOffset >= 0)
ApplyResult(GetCappedResult(HitResult.Perfect));
else
base.CheckForResult(userTriggered, timeOffset);
}
}
private class NoReleaseTailNote : TailNote
{
}
private class NoReleaseHoldNote : HoldNote
{
public NoReleaseHoldNote(HoldNote hold)
{
StartTime = hold.StartTime;
Duration = hold.Duration;
Column = hold.Column;
NodeSamples = hold.NodeSamples;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
AddNested(Head = new HeadNote
{
StartTime = StartTime,
Column = Column,
Samples = GetNodeSamples(0),
});
AddNested(Tail = new NoReleaseTailNote
{
StartTime = EndTime,
Column = Column,
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
AddNested(Body = new HoldNoteBody
{
StartTime = StartTime,
Column = Column
});
}
}
}
}

View File

@ -273,11 +273,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyMaxResult(); ApplyMaxResult();
else else
MissForcefully(); MissForcefully();
}
// Make sure that the hold note is fully judged by giving the body a judgement. // Make sure that the hold note is fully judged by giving the body a judgement.
if (Tail.AllJudged && !Body.AllJudged) if (!Body.AllJudged)
Body.TriggerResult(Tail.IsHit); Body.TriggerResult(Tail.IsHit);
// Important that this is always called when a result is applied.
endHold();
}
} }
public override void MissForcefully() public override void MissForcefully()

View File

@ -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;
@ -72,18 +73,18 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary> /// <summary>
/// The head note of the hold. /// The head note of the hold.
/// </summary> /// </summary>
public HeadNote Head { get; private set; } public HeadNote Head { get; protected set; }
/// <summary> /// <summary>
/// The tail note of the hold. /// The tail note of the hold.
/// </summary> /// </summary>
public TailNote Tail { get; private set; } public TailNote Tail { get; protected set; }
/// <summary> /// <summary>
/// The body of the hold. /// The body of the hold.
/// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>. /// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>.
/// </summary> /// </summary>
public HoldNoteBody Body { get; private set; } public HoldNoteBody Body { get; protected set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
@ -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>(),
};
} }
} }

View File

@ -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;
} }
} }

View File

@ -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));
}
} }
} }

View File

@ -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 _);
} }
} }

View File

@ -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);
}
}
}

View File

@ -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();

View File

@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
// i dunno this looks about right?? // i dunno this looks about right??
// the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild. // the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild.
if (sprite.DrawHeight > 0) if (sprite.DrawHeight > 0)
bodySprite.Scale = new Vector2(1, MathF.Max(1, scaleDirection * 32800 / sprite.DrawHeight)); bodySprite.Scale = new Vector2(1, scaleDirection * MathF.Max(1, 32800 / sprite.DrawHeight));
} }
break; break;

View File

@ -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;
}
}
}

View File

@ -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:

View File

@ -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

View File

@ -8,9 +8,10 @@ using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
@ -56,13 +57,18 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>(); private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableInt configScrollSpeed = new BindableInt(); private readonly BindableInt configScrollSpeed = new BindableInt();
private double smoothTimeRange;
private double currentTimeRange;
protected double TargetTimeRange;
// Stores the current speed adjustment active in gameplay. // Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0); private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
private ISkinSource currentSkin = null!; private ISkinSource currentSkin = null!;
[Resolved]
private GameHost gameHost { get; set; } = null!;
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
@ -101,9 +107,9 @@ namespace osu.Game.Rulesets.Mania.UI
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed); Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint)); configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
KeyBindingInputManager.Add(new ManiaTouchInputArea()); KeyBindingInputManager.Add(new ManiaTouchInputArea());
} }
@ -144,7 +150,9 @@ namespace osu.Game.Rulesets.Mania.UI
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position. // This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
float scale = lengthToHitPosition / length_to_default_hit_position; float scale = lengthToHitPosition / length_to_default_hit_position;
TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale; // we're intentionally using the game host's update clock here to decouple the time range tween from the gameplay clock (which can be arbitrarily paused, or even rewinding)
currentTimeRange = Interpolation.DampContinuously(currentTimeRange, TargetTimeRange, 50, gameHost.UpdateThread.Clock.ElapsedFrameTime);
TimeRange.Value = currentTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
} }
/// <summary> /// <summary>

View File

@ -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;

View File

@ -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)
@ -192,12 +175,12 @@ namespace osu.Game.Rulesets.Mania.UI
if (press) if (press)
{ {
inputManager?.KeyBindingContainer?.TriggerPressed(Action.Value); inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint); highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
} }
else else
{ {
inputManager?.KeyBindingContainer?.TriggerReleased(Action.Value); inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
highlightOverlay.FadeTo(0, 400, Easing.OutQuint); highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
} }
} }

View File

@ -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());

View File

@ -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;
} }
} }

View File

@ -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();

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -23,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);
} }
@ -56,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());
@ -69,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());
@ -160,6 +161,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return grid switch return grid switch
{ {
RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value), RectangularPositionSnapGrid rectangular => rectangular.StartPosition.Value + GeometryUtils.RotateVector(rectangular.Spacing.Value, -rectangular.GridLineRotation.Value),
TriangularPositionSnapGrid triangular => triangular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(triangular.Spacing.Value / 2, triangular.Spacing.Value / 2 * MathF.Sqrt(3)), -triangular.GridLineRotation.Value),
CircularPositionSnapGrid circular => circular.StartPosition.Value + GeometryUtils.RotateVector(new Vector2(circular.Spacing.Value, 0), -45),
_ => Vector2.Zero _ => Vector2.Zero
}; };
} }
@ -167,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);

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
@ -177,6 +178,79 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addAssertPointPositionChanged(points, i); addAssertPointPositionChanged(points, i);
} }
[Test]
public void TestChangingControlPointTypeViaTab()
{
createVisualiser(true);
addControlPointStep(new Vector2(200), PathType.LINEAR);
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
addControlPointStep(new Vector2(700, 200));
addControlPointStep(new Vector2(500, 100));
AddStep("select first control point", () => visualiser.Pieces[0].IsSelected.Value = true);
AddStep("press tab", () => InputManager.Key(Key.Tab));
assertControlPointPathType(0, PathType.BEZIER);
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.LShift);
});
assertControlPointPathType(0, PathType.LINEAR);
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.LShift);
});
assertControlPointPathType(0, PathType.BSpline(4));
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.LShift);
});
assertControlPointPathType(0, PathType.PERFECT_CURVE);
assertControlPointPathType(2, PathType.BSpline(4));
AddStep("select third last control point", () =>
{
visualiser.Pieces[0].IsSelected.Value = false;
visualiser.Pieces[2].IsSelected.Value = true;
});
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.LShift);
});
assertControlPointPathType(2, PathType.PERFECT_CURVE);
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
assertControlPointPathType(0, PathType.BEZIER);
assertControlPointPathType(2, null);
AddStep("select first and third control points", () =>
{
visualiser.Pieces[0].IsSelected.Value = true;
visualiser.Pieces[2].IsSelected.Value = true;
});
AddStep("press alt-1", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Number1);
InputManager.ReleaseKey(Key.AltLeft);
});
assertControlPointPathType(0, PathType.LINEAR);
assertControlPointPathType(2, PathType.LINEAR);
}
private void addAssertPointPositionChanged(Vector2[] points, int index) private void addAssertPointPositionChanged(Vector2[] points, int index)
{ {
AddAssert($"Point at {points.ElementAt(index)} changed", AddAssert($"Point at {points.ElementAt(index)} changed",

View File

@ -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());
}
}

View File

@ -2,13 +2,16 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
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.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(200); assertLength(200);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
} }
[Test] [Test]
@ -71,7 +74,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
} }
[Test] [Test]
@ -89,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -111,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100, 100)); assertControlPointPosition(2, new Vector2(100, 100));
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -130,8 +133,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR); assertFinalControlPointType(1, PathType.LINEAR);
} }
[Test] [Test]
@ -149,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(2); assertControlPointCount(2);
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertLength(100); assertLength(100);
} }
@ -171,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -195,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -215,8 +218,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.LINEAR); assertFinalControlPointType(1, PathType.LINEAR);
} }
[Test] [Test]
@ -239,8 +242,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(1, new Vector2(100, 0));
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointType(0, PathType.LINEAR); assertFinalControlPointType(0, PathType.LINEAR);
assertControlPointType(1, PathType.PERFECT_CURVE); assertFinalControlPointType(1, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -268,8 +271,54 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(2, new Vector2(100));
assertControlPointPosition(3, new Vector2(200, 100)); assertControlPointPosition(3, new Vector2(200, 100));
assertControlPointPosition(4, new Vector2(200)); assertControlPointPosition(4, new Vector2(200));
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
assertControlPointType(2, PathType.PERFECT_CURVE); assertFinalControlPointType(2, PathType.PERFECT_CURVE);
}
[Test]
public void TestManualPathTypeControlViaKeyboard()
{
addMovementStep(new Vector2(200));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(300, 200));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(300));
assertControlPointTypeDuringPlacement(0, PathType.PERFECT_CURVE);
AddRepeatStep("press tab", () => InputManager.Key(Key.Tab), 2);
assertControlPointTypeDuringPlacement(0, PathType.LINEAR);
AddStep("press shift-tab", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Tab);
InputManager.ReleaseKey(Key.ShiftLeft);
});
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));
assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
addMovementStep(new Vector2(400, 300));
addClickStep(MouseButton.Left);
addMovementStep(new Vector2(400));
addClickStep(MouseButton.Right);
assertPlaced(true);
assertFinalControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -293,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Right); addClickStep(MouseButton.Right);
assertPlaced(true); assertPlaced(true);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -312,11 +361,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(808, tolerance: 10); assertLength(808, tolerance: 10);
assertControlPointCount(5); assertControlPointCount(5);
assertControlPointType(0, PathType.BSpline(4)); assertFinalControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, null); assertFinalControlPointType(1, null);
assertControlPointType(2, null); assertFinalControlPointType(2, null);
assertControlPointType(3, null); assertFinalControlPointType(3, null);
assertControlPointType(4, null); assertFinalControlPointType(4, null);
} }
[Test] [Test]
@ -337,10 +386,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertLength(600, tolerance: 10); assertLength(600, tolerance: 10);
assertControlPointCount(4); assertControlPointCount(4);
assertControlPointType(0, PathType.BSpline(4)); assertFinalControlPointType(0, PathType.BSpline(4));
assertControlPointType(1, PathType.BSpline(4)); assertFinalControlPointType(1, PathType.BSpline(4));
assertControlPointType(2, PathType.BSpline(4)); assertFinalControlPointType(2, PathType.BSpline(4));
assertControlPointType(3, null); assertFinalControlPointType(3, null);
} }
[Test] [Test]
@ -359,7 +408,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -379,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -400,7 +449,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
[Test] [Test]
@ -421,7 +470,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.BEZIER); assertFinalControlPointType(0, PathType.BEZIER);
} }
[Test] [Test]
@ -438,7 +487,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertPlaced(true); assertPlaced(true);
assertControlPointCount(3); assertControlPointCount(3);
assertControlPointType(0, PathType.PERFECT_CURVE); assertFinalControlPointType(0, PathType.PERFECT_CURVE);
} }
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
@ -454,7 +503,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected)); private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected));
private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type)); private void assertControlPointTypeDuringPlacement(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}",
() => this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(index).ControlPoint.Type, () => Is.EqualTo(type));
private void assertFinalControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type));
private void assertControlPointPosition(int index, Vector2 position) => private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1)); AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1));

View File

@ -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", () =>

View File

@ -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();

View 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;
}
}
}

View File

@ -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.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
else else
applyStackingOld(Beatmap.BeatmapInfo, hitObjects); applyStackingOld(beatmap.BeatmapInfo, hitObjects);
} }
} }
private void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex) private static void applyStacking(BeatmapInfo beatmapInfo, 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(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects) private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
{ {
for (int i = 0; i < hitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {

View File

@ -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,
} }
} }

View File

@ -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;
} }
} }
} }

View File

@ -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(

View File

@ -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)

View File

@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
} }
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER; public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
} }
} }

View File

@ -67,5 +67,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return difficulty * DifficultyMultiplier; return difficulty * DifficultyMultiplier;
} }
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
} }
} }

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