mirror of
https://github.com/ppy/osu.git
synced 2024-11-15 16:22:21 +08:00
Merge branch 'master' into move-already-placed-objects-when-adjusting-offset-bpm
This commit is contained in:
commit
75745bd95f
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -135,5 +135,8 @@ jobs:
|
||||
- name: Install .NET Workloads
|
||||
run: dotnet workload install maui-ios
|
||||
|
||||
- name: Select Xcode 16
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c Debug osu.iOS
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.831.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.927.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -80,7 +80,7 @@ namespace osu.Android
|
||||
host.Window.CursorState |= CursorState.Hidden;
|
||||
}
|
||||
|
||||
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
||||
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
|
||||
|
||||
|
@ -2,10 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using osu.Desktop.Performance;
|
||||
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", "");
|
||||
}
|
||||
|
||||
public static bool IsPackageManaged => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"));
|
||||
|
||||
protected override UpdateManager CreateUpdateManager()
|
||||
{
|
||||
string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER");
|
||||
|
||||
if (!string.IsNullOrEmpty(packageManaged))
|
||||
if (IsPackageManaged)
|
||||
return new NoActionUpdateManager();
|
||||
|
||||
switch (RuntimeInfo.OS)
|
||||
{
|
||||
case RuntimeInfo.Platform.Windows:
|
||||
Debug.Assert(OperatingSystem.IsWindows());
|
||||
|
||||
return new SquirrelUpdateManager();
|
||||
|
||||
default:
|
||||
return new SimpleUpdateManager();
|
||||
}
|
||||
return new VelopackUpdateManager();
|
||||
}
|
||||
|
||||
public override bool RestartAppWhenExited()
|
||||
{
|
||||
switch (RuntimeInfo.OS)
|
||||
{
|
||||
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();
|
||||
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -14,7 +14,7 @@ using osu.Game;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Tournament;
|
||||
using SDL;
|
||||
using Squirrel;
|
||||
using Velopack;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
@ -31,19 +31,11 @@ namespace osu.Desktop
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
/*
|
||||
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
|
||||
*
|
||||
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
|
||||
* 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")
|
||||
*/
|
||||
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
|
||||
// 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.
|
||||
setupVelopack();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var windowsVersion = Environment.OSVersion.Version;
|
||||
@ -66,8 +58,6 @@ namespace osu.Desktop
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setupSquirrel();
|
||||
}
|
||||
|
||||
// NVIDIA profiles are based on the executable name of a process.
|
||||
@ -177,32 +167,28 @@ namespace osu.Desktop
|
||||
return false;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void setupSquirrel()
|
||||
private static void setupVelopack()
|
||||
{
|
||||
SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) =>
|
||||
if (OsuGameDesktop.IsPackageManaged)
|
||||
{
|
||||
tools.CreateShortcutForThisExe();
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.InstallAssociations();
|
||||
}, onAppUpdate: (_, tools) =>
|
||||
{
|
||||
tools.CreateUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UpdateAssociations();
|
||||
}, onAppUninstall: (_, tools) =>
|
||||
{
|
||||
tools.RemoveShortcutForThisExe();
|
||||
tools.RemoveUninstallerRegistryEntry();
|
||||
WindowsAssociationManager.UninstallAssociations();
|
||||
}, onEveryRun: (_, _, _) =>
|
||||
{
|
||||
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
|
||||
// causes the right-click context menu to function incorrectly.
|
||||
//
|
||||
// This may turn out to be non-required after an alternative solution is implemented.
|
||||
// see https://github.com/clowd/Clowd.Squirrel/issues/24
|
||||
// tools.SetProcessAppUserModelId();
|
||||
});
|
||||
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
|
||||
return;
|
||||
}
|
||||
|
||||
var app = VelopackApp.Build();
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
configureWindows(app);
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void configureWindows(VelopackApp app)
|
||||
{
|
||||
app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
||||
app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
||||
app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
140
osu.Desktop/Updater/VelopackUpdateManager.cs
Normal file
140
osu.Desktop/Updater/VelopackUpdateManager.cs
Normal file
@ -0,0 +1,140 @@
|
||||
// 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 = () =>
|
||||
{
|
||||
Task.Run(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 = () =>
|
||||
{
|
||||
Task.Run(restartToApplyUpdate);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
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 async Task restartToApplyUpdate()
|
||||
{
|
||||
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
|
||||
Schedule(() => game.AttemptExit());
|
||||
}
|
||||
}
|
||||
}
|
@ -13,5 +13,7 @@ namespace osu.Desktop.Windows
|
||||
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
|
||||
|
||||
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
|
||||
|
||||
public static string Beatmap => Path.Join(icon_directory, "beatmap.ico");
|
||||
}
|
||||
}
|
||||
|
@ -40,10 +40,10 @@ namespace osu.Desktop.Windows
|
||||
|
||||
private static readonly FileAssociation[] file_associations =
|
||||
{
|
||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
|
||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
|
||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
|
||||
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap),
|
||||
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap),
|
||||
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Beatmap),
|
||||
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Beatmap),
|
||||
};
|
||||
|
||||
private static readonly UriAssociation[] uri_associations =
|
||||
|
BIN
osu.Desktop/beatmap.ico
Normal file
BIN
osu.Desktop/beatmap.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 349 KiB |
@ -5,6 +5,7 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
|
||||
<AssemblyName>osu!</AssemblyName>
|
||||
<AssemblyTitle>osu!(lazer)</AssemblyTitle>
|
||||
<Title>osu!</Title>
|
||||
<Product>osu!(lazer)</Product>
|
||||
<ApplicationIcon>lazer.ico</ApplicationIcon>
|
||||
@ -23,9 +24,9 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.11.1" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
29
osu.Game.Benchmarks/BenchmarkGeometryUtils.cs
Normal file
29
osu.Game.Benchmarks/BenchmarkGeometryUtils.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkGeometryUtils : BenchmarkTest
|
||||
{
|
||||
[Params(100, 1000, 2000, 4000, 8000, 10000)]
|
||||
public int N;
|
||||
|
||||
private Vector2[] points = null!;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
points = new Vector2[N];
|
||||
|
||||
for (int i = 0; i < points.Length; ++i)
|
||||
points[i] = new Vector2(RNG.Next(512), RNG.Next(384));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void MinimumEnclosingCircle() => GeometryUtils.MinimumEnclosingCircle(points);
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
{
|
||||
var result = base.SnapForBlueprint(blueprint);
|
||||
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
|
||||
|
||||
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
|
||||
protected override void AddHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
|
||||
|
||||
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
|
||||
|
||||
[Test]
|
||||
public void TestFruitPlacementPosition()
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject);
|
||||
|
||||
protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
|
||||
private void addMoveAndClickSteps(double time, float position, bool end = false)
|
||||
{
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double star_scaling_factor = 0.153;
|
||||
private const double difficulty_multiplier = 4.59;
|
||||
|
||||
private float halfCatcherWidth;
|
||||
|
||||
@ -41,10 +40,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||
{
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
return attributes;
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
private const float normalized_hitobject_radius = 41.0f;
|
||||
private const double direction_change_bonus = 21.0;
|
||||
|
||||
protected override double SkillMultiplier => 900;
|
||||
protected override double SkillMultiplier => 1;
|
||||
protected override double StrainDecayBase => 0.2;
|
||||
|
||||
protected override double DecayWeight => 0.94;
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class BananaShowerCompositionTool : HitObjectCompositionTool
|
||||
public class BananaShowerCompositionTool : CompositionTool
|
||||
{
|
||||
public BananaShowerCompositionTool()
|
||||
: base(nameof(BananaShower))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public partial class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
|
||||
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||
where THitObject : CatchHitObject, new()
|
||||
{
|
||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||
|
@ -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.
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid();
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||
{
|
||||
new FruitCompositionTool(),
|
||||
new JuiceStreamCompositionTool(),
|
||||
@ -114,6 +114,26 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
handleToggleViaKey(e);
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
handleToggleViaKey(e);
|
||||
base.OnKeyUp(e);
|
||||
}
|
||||
|
||||
private void handleToggleViaKey(KeyboardEvent key)
|
||||
{
|
||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class FruitCompositionTool : HitObjectCompositionTool
|
||||
public class FruitCompositionTool : CompositionTool
|
||||
{
|
||||
public FruitCompositionTool()
|
||||
: base(nameof(Fruit))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class JuiceStreamCompositionTool : HitObjectCompositionTool
|
||||
public class JuiceStreamCompositionTool : CompositionTool
|
||||
{
|
||||
public JuiceStreamCompositionTool()
|
||||
: base(nameof(JuiceStream))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
{
|
||||
@ -36,23 +38,43 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
StartTimeBindable.BindValueChanged(_ => UpdateComboColour());
|
||||
}
|
||||
|
||||
private float startScale;
|
||||
private float endScale;
|
||||
|
||||
private float startAngle;
|
||||
private float endAngle;
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||
const float end_scale = 0.6f;
|
||||
const float random_scale_range = 1.6f;
|
||||
|
||||
ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3)))
|
||||
.Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt);
|
||||
startScale = end_scale + random_scale_range * RandomSingle(3);
|
||||
endScale = end_scale;
|
||||
|
||||
ScalingContainer.RotateTo(getRandomAngle(1))
|
||||
.Then()
|
||||
.RotateTo(getRandomAngle(2), HitObject.TimePreempt);
|
||||
startAngle = getRandomAngle(1);
|
||||
endAngle = getRandomAngle(2);
|
||||
|
||||
float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt;
|
||||
|
||||
// Clamp scale and rotation at the point of bananas being caught, else let them freely extrapolate.
|
||||
if (Result.IsHit)
|
||||
preemptProgress = Math.Min(1, preemptProgress);
|
||||
|
||||
ScalingContainer.Scale = new Vector2(HitObject.Scale * (float)Interpolation.Lerp(startScale, endScale, preemptProgress));
|
||||
ScalingContainer.Rotation = (float)Interpolation.Lerp(startAngle, endAngle, preemptProgress);
|
||||
}
|
||||
|
||||
public override void PlaySamples()
|
||||
{
|
||||
base.PlaySamples();
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -28,15 +28,24 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
_ => new DropletPiece());
|
||||
}
|
||||
|
||||
private float startRotation;
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
// roughly matches osu-stable
|
||||
float startRotation = RandomSingle(1) * 20;
|
||||
double duration = HitObject.TimePreempt + 2000;
|
||||
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||
startRotation = RandomSingle(1) * 20;
|
||||
}
|
||||
|
||||
ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration);
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// No clamping for droplets. They should be considered indefinitely spinning regardless of time.
|
||||
// They also never end up on the plate, so they shouldn't stop spinning when caught.
|
||||
double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / (HitObject.TimePreempt + 2000);
|
||||
ScalingContainer.Rotation = (float)Interpolation.Lerp(startRotation, startRotation + 720, preemptProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -32,7 +31,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40);
|
||||
// Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms().
|
||||
ScalingContainer.Rotation = (RandomSingle(1) - 0.5f) * 40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
});
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
{
|
||||
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
var pos = column.ScreenSpacePositionAtTime(time);
|
||||
|
@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -92,5 +93,30 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
|
||||
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOffScreenObjectsRemainSelectedOnColumnChange()
|
||||
{
|
||||
AddStep("create objects", () =>
|
||||
{
|
||||
for (int i = 0; i < 20; ++i)
|
||||
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = 0 });
|
||||
});
|
||||
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("start drag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First());
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("end drag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last());
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
|
||||
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
public class ManiaDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double star_scaling_factor = 0.018;
|
||||
private const double difficulty_multiplier = 0.018;
|
||||
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||
StarRating = skills[0].DifficultyValue() * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
|
@ -38,9 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
scoreAccuracy = calculateCustomAccuracy();
|
||||
|
||||
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
|
||||
// The specific number has no intrinsic meaning and can be adjusted as needed.
|
||||
double multiplier = 8.0;
|
||||
double multiplier = 1.0;
|
||||
|
||||
if (score.Mods.Any(m => m is ModNoFail))
|
||||
multiplier *= 0.75;
|
||||
@ -59,9 +57,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
|
||||
{
|
||||
double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
|
||||
* Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
|
||||
* (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
|
||||
double difficultyValue = 8.0 * Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve
|
||||
* Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
|
||||
* (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes
|
||||
|
||||
return difficultyValue;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
public abstract partial class ManiaPlacementBlueprint<T> : PlacementBlueprint
|
||||
public abstract partial class ManiaPlacementBlueprint<T> : HitObjectPlacementBlueprint
|
||||
where T : ManiaHitObject
|
||||
{
|
||||
protected new T HitObject => (T)base.HitObject;
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class HoldNoteCompositionTool : HitObjectCompositionTool
|
||||
public class HoldNoteCompositionTool : CompositionTool
|
||||
{
|
||||
public HoldNoteCompositionTool()
|
||||
: base("Hold")
|
||||
@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid();
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||
{
|
||||
new NoteCompositionTool(),
|
||||
new HoldNoteCompositionTool()
|
||||
|
@ -104,8 +104,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
int minColumn = int.MaxValue;
|
||||
int maxColumn = int.MinValue;
|
||||
|
||||
var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>().ToArray();
|
||||
|
||||
// find min/max in an initial pass before actually performing the movement.
|
||||
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
|
||||
foreach (var obj in selectedObjects)
|
||||
{
|
||||
if (obj.Column < minColumn)
|
||||
minColumn = obj.Column;
|
||||
@ -121,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
((ManiaHitObject)h).Column += columnDelta;
|
||||
maniaPlayfield.Add(h);
|
||||
});
|
||||
|
||||
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern,
|
||||
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
|
||||
// (check blame for detailed explanation).
|
||||
// thus, ensure that selection is preserved manually.
|
||||
EditorBeatmap.SelectedHitObjects.Clear();
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(selectedObjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class NoteCompositionTool : HitObjectCompositionTool
|
||||
public class NoteCompositionTool : CompositionTool
|
||||
{
|
||||
public NoteCompositionTool()
|
||||
: base(nameof(Note))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,12 @@
|
||||
// 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;
|
||||
@ -12,17 +15,76 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyManiaComboCounter : LegacyComboCounter
|
||||
public partial class LegacyManiaComboCounter : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
DisplayedCountText.Anchor = Anchor.Centre;
|
||||
DisplayedCountText.Origin = Anchor.Centre;
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
PopOutCountText.Anchor = Anchor.Centre;
|
||||
PopOutCountText.Origin = Anchor.Centre;
|
||||
PopOutCountText.Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red;
|
||||
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]
|
||||
@ -34,6 +96,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
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());
|
||||
|
||||
@ -56,36 +124,71 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
|
||||
}
|
||||
|
||||
protected override void OnCountIncrement()
|
||||
private void updateCount(bool rolling)
|
||||
{
|
||||
base.OnCountIncrement();
|
||||
int prev = previousValue;
|
||||
previousValue = Current.Value;
|
||||
|
||||
PopOutCountText.Hide();
|
||||
DisplayedCountText.ScaleTo(new Vector2(1f, 1.4f))
|
||||
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);
|
||||
}
|
||||
|
||||
protected override void OnCountChange()
|
||||
private void onCountChange()
|
||||
{
|
||||
base.OnCountChange();
|
||||
popOutCountText.Hide();
|
||||
|
||||
PopOutCountText.Hide();
|
||||
DisplayedCountText.ScaleTo(1f);
|
||||
if (Current.Value == 0)
|
||||
displayedCountText.FadeOut();
|
||||
|
||||
DisplayedCount = Current.Value;
|
||||
|
||||
displayedCountText.ScaleTo(1f);
|
||||
}
|
||||
|
||||
protected override void OnCountRolling()
|
||||
private void onCountRolling()
|
||||
{
|
||||
if (DisplayedCount > 0)
|
||||
{
|
||||
PopOutCountText.Text = FormatCount(DisplayedCount);
|
||||
PopOutCountText.FadeTo(0.8f).FadeOut(200)
|
||||
popOutCountText.Text = DisplayedCount.ToString(CultureInfo.InvariantCulture);
|
||||
popOutCountText.FadeTo(0.8f).FadeOut(200)
|
||||
.ScaleTo(1f).ScaleTo(4f, 200);
|
||||
|
||||
DisplayedCountText.FadeTo(0.5f, 300);
|
||||
displayedCountText.FadeTo(0.5f, 300);
|
||||
}
|
||||
|
||||
base.OnCountRolling();
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
|
||||
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
|
||||
|
||||
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
|
||||
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(2).TriggerClick());
|
||||
AddAssert("first object rotated 90deg around selection centre",
|
||||
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
|
||||
AddAssert("second object rotated 90deg around selection centre",
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -514,6 +514,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
|
||||
protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -22,9 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
|
||||
{
|
||||
private Slider slider;
|
||||
private DrawableSlider drawableObject;
|
||||
private TestSliderBlueprint blueprint;
|
||||
private Slider slider = null!;
|
||||
private DrawableSlider drawableObject = null!;
|
||||
private TestSliderBlueprint blueprint = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
@ -218,6 +216,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("tail positioned correctly",
|
||||
() => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddAssert("end drag marker positioned correctly",
|
||||
() => Precision.AlmostEquals(blueprint.TailOverlay.EndDragMarker!.ToScreenSpace(blueprint.TailOverlay.EndDragMarker.OriginPosition), drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre, 2));
|
||||
}
|
||||
|
||||
private void moveMouseToControlPoint(int index)
|
||||
@ -230,14 +231,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
}
|
||||
|
||||
private void checkControlPointSelected(int index, bool selected)
|
||||
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected);
|
||||
=> AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser!.Pieces[index].IsSelected.Value == selected);
|
||||
|
||||
private partial class TestSliderBlueprint : SliderSelectionBlueprint
|
||||
{
|
||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
|
||||
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
|
||||
public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
public new PathControlPointVisualiser<Slider>? ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
|
||||
public TestSliderBlueprint(Slider slider)
|
||||
: base(slider)
|
||||
|
@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
|
||||
|
||||
protected override PlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
133
osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs
Normal file
133
osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs
Normal file
@ -0,0 +1,133 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneOsuAnalysisContainer : OsuTestScene
|
||||
{
|
||||
private TestReplayAnalysisOverlay analysisContainer = null!;
|
||||
private ReplayAnalysisSettings settings = null!;
|
||||
|
||||
[Cached]
|
||||
private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo);
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create analysis container", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuPlayfieldAdjustmentContainer
|
||||
{
|
||||
Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()),
|
||||
},
|
||||
settings = new ReplayAnalysisSettings(config),
|
||||
};
|
||||
|
||||
settings.ShowClickMarkers.Value = false;
|
||||
settings.ShowAimMarkers.Value = false;
|
||||
settings.ShowCursorPath.Value = false;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEverythingOn()
|
||||
{
|
||||
AddStep("enable everything", () =>
|
||||
{
|
||||
settings.ShowClickMarkers.Value = true;
|
||||
settings.ShowAimMarkers.Value = true;
|
||||
settings.ShowCursorPath.Value = true;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitMarkers()
|
||||
{
|
||||
AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true);
|
||||
AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible);
|
||||
AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false);
|
||||
AddUntilStep("hit markers not visible", () => !analysisContainer.HitMarkersVisible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAimMarker()
|
||||
{
|
||||
AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true);
|
||||
AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible);
|
||||
AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false);
|
||||
AddUntilStep("aim markers not visible", () => !analysisContainer.AimMarkersVisible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAimLines()
|
||||
{
|
||||
AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true);
|
||||
AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible);
|
||||
AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false);
|
||||
AddUntilStep("aim lines not visible", () => !analysisContainer.AimLinesVisible);
|
||||
}
|
||||
|
||||
private Replay fabricateReplay()
|
||||
{
|
||||
var frames = new List<ReplayFrame>();
|
||||
var random = new Random();
|
||||
int posX = 250;
|
||||
int posY = 250;
|
||||
|
||||
var actions = new HashSet<OsuAction>();
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
posX = Math.Clamp(posX + random.Next(-20, 21), -100, 600);
|
||||
posY = Math.Clamp(posY + random.Next(-20, 21), -100, 600);
|
||||
|
||||
if (random.NextDouble() > (actions.Count == 0 ? 0.9 : 0.95))
|
||||
{
|
||||
actions.Add(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton);
|
||||
}
|
||||
else if (random.NextDouble() > 0.7)
|
||||
{
|
||||
actions.Remove(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton);
|
||||
}
|
||||
|
||||
frames.Add(new OsuReplayFrame
|
||||
{
|
||||
Time = Time.Current + i * 15,
|
||||
Position = new Vector2(posX, posY),
|
||||
Actions = actions.ToList(),
|
||||
});
|
||||
}
|
||||
|
||||
return new Replay { Frames = frames };
|
||||
}
|
||||
|
||||
private partial class TestReplayAnalysisOverlay : ReplayAnalysisOverlay
|
||||
{
|
||||
public TestReplayAnalysisOverlay(Replay replay)
|
||||
: base(replay)
|
||||
{
|
||||
}
|
||||
|
||||
public bool HitMarkersVisible => ClickMarkers?.Alpha > 0 && ClickMarkers.Entries.Any();
|
||||
public bool AimMarkersVisible => FrameMarkers?.Alpha > 0 && FrameMarkers.Entries.Any();
|
||||
public bool AimLinesVisible => CursorPath?.Alpha > 0 && CursorPath.Vertices.Count > 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -11,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
||||
{
|
||||
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)
|
||||
{
|
||||
}
|
||||
@ -24,6 +22,12 @@ namespace osu.Game.Rulesets.Osu.Configuration
|
||||
SetDefault(OsuRulesetSetting.ShowCursorTrail, true);
|
||||
SetDefault(OsuRulesetSetting.ShowCursorRipples, false);
|
||||
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,
|
||||
ShowCursorRipples,
|
||||
PlayfieldBorderStyle,
|
||||
|
||||
// Replay
|
||||
ReplayClickMarkersEnabled,
|
||||
ReplayFrameMarkersEnabled,
|
||||
ReplayCursorPathEnabled,
|
||||
ReplayCursorHideEnabled,
|
||||
ReplayAnalysisDisplayLength,
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
{
|
||||
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 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.
|
||||
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;
|
||||
|
||||
// Add additional scaling bonus for streams/bursts higher than 200bpm
|
||||
if (strainTime < min_speed_bonus)
|
||||
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
int maxCombo = beatmap.GetMaxCombo();
|
||||
|
||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = maxCombo,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
HitCircleCount = hitCirclesCount,
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 23.55;
|
||||
private double skillMultiplier => 24.963;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
hasHiddenMod = mods.Any(m => m is OsuModHidden);
|
||||
}
|
||||
|
||||
private double skillMultiplier => 0.052;
|
||||
private double skillMultiplier => 0.05512;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private double currentStrain;
|
||||
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
|
||||
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum();
|
||||
|
||||
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
|
||||
}
|
||||
|
@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
public abstract class OsuStrainSkill : StrainSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// The default multiplier applied by <see cref="OsuStrainSkill"/> to the final difficulty value after all other calculations.
|
||||
/// May be overridden via <see cref="DifficultyMultiplier"/>.
|
||||
/// </summary>
|
||||
public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06;
|
||||
|
||||
/// <summary>
|
||||
/// The number of sections with the highest strains, which the peak strain reductions will apply to.
|
||||
/// This is done in order to decrease their impact on the overall difficulty of the map for this skill.
|
||||
@ -29,11 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
protected virtual double ReducedStrainBaseline => 0.75;
|
||||
|
||||
/// <summary>
|
||||
/// The final multiplier to be applied to <see cref="DifficultyValue"/> after all other calculations.
|
||||
/// </summary>
|
||||
protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER;
|
||||
|
||||
protected OsuStrainSkill(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
@ -65,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
weight *= DecayWeight;
|
||||
}
|
||||
|
||||
return difficulty * DifficultyMultiplier;
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
@ -16,14 +16,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Speed : OsuStrainSkill
|
||||
{
|
||||
private double skillMultiplier => 1375;
|
||||
private double skillMultiplier => 1.430;
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain;
|
||||
private double currentRhythm;
|
||||
|
||||
protected override int ReducedSectionCount => 5;
|
||||
protected override double DifficultyMultiplier => 1.04;
|
||||
|
||||
private readonly List<double> objectStrains = new List<double>();
|
||||
|
||||
|
@ -9,7 +9,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
{
|
||||
public partial class HitCirclePlacementBlueprint : PlacementBlueprint
|
||||
public partial class HitCirclePlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
public new HitCircle HitObject => (HitCircle)base.HitObject;
|
||||
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
if (endDragMarkerContainer != null)
|
||||
{
|
||||
endDragMarkerContainer.Position = circle.Position;
|
||||
endDragMarkerContainer.Position = circle.Position + slider.StackOffset;
|
||||
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
|
||||
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
|
||||
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
|
||||
|
@ -21,7 +21,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public partial class SliderPlacementBlueprint : PlacementBlueprint
|
||||
public partial class SliderPlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
public new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
|
@ -13,7 +13,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
|
||||
{
|
||||
public partial class SpinnerPlacementBlueprint : PlacementBlueprint
|
||||
public partial class SpinnerPlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
public new Spinner HitObject => (Spinner)base.HitObject;
|
||||
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class HitCircleCompositionTool : HitObjectCompositionTool
|
||||
public class HitCircleCompositionTool : CompositionTool
|
||||
{
|
||||
public HitCircleCompositionTool()
|
||||
: base(nameof(HitCircle))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
protected override DrawableRuleset<OsuHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new DrawableOsuEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||
{
|
||||
new HitCircleCompositionTool(),
|
||||
new SliderCompositionTool(),
|
||||
@ -106,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||
GridToolbox = OsuGridToolboxGroup,
|
||||
},
|
||||
new GenerateToolboxGroup(),
|
||||
FreehandSliderToolboxGroup
|
||||
@ -368,6 +369,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
gridSnapMomentary = shiftPressed;
|
||||
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||
}
|
||||
|
||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||
}
|
||||
|
||||
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -25,6 +26,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
[Resolved]
|
||||
private OsuGridToolboxGroup gridToolbox { get; set; } = null!;
|
||||
|
||||
protected override void OnSelectionChanged()
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
@ -123,13 +127,43 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
// If we're flipping over the origin, we take the grid origin position from the grid toolbox.
|
||||
var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
Vector2 flipAxis = direction == Direction.Vertical ? Vector2.UnitY : Vector2.UnitX;
|
||||
|
||||
if (flipOverOrigin)
|
||||
{
|
||||
// If we're flipping over the origin, we take one of the axes of the grid.
|
||||
// Take the axis closest to the direction we want to flip over.
|
||||
switch (gridToolbox.GridType.Value)
|
||||
{
|
||||
case PositionSnapGridType.Square:
|
||||
flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 45) % 90 - 45));
|
||||
flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis;
|
||||
break;
|
||||
|
||||
case PositionSnapGridType.Triangle:
|
||||
// Hex grid has 3 axes, so you can not directly flip over one of the axes,
|
||||
// however it's still possible to achieve that flip by combining multiple flips over the other axes.
|
||||
// Angle degree range for vertical = (-120, -60]
|
||||
// Angle degree range for horizontal = [-30, 30)
|
||||
flipAxis = direction == Direction.Vertical
|
||||
? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 30) % 60 + 60))
|
||||
: GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var controlPointFlipQuad = new Quad();
|
||||
|
||||
bool didFlip = false;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
|
||||
var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position);
|
||||
|
||||
// Clamp the flipped position inside the playfield bounds, because the flipped position might be outside the playfield bounds if the origin is not centered.
|
||||
flippedPosition = Vector2.Clamp(flippedPosition, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||
|
||||
if (!Precision.AlmostEquals(flippedPosition, h.Position))
|
||||
{
|
||||
@ -142,12 +176,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
didFlip = true;
|
||||
|
||||
foreach (var cp in slider.Path.ControlPoints)
|
||||
{
|
||||
cp.Position = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * cp.Position.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * cp.Position.Y
|
||||
);
|
||||
}
|
||||
cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private OsuHitObject[]? objectsInRotation;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
||||
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
||||
|
||||
@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInRotation = selectedMovableObjects.ToArray();
|
||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
|
||||
DefaultOrigin = GeometryUtils.MinimumEnclosingCircle(objectsInRotation).Item1;
|
||||
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
|
||||
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
|
||||
obj => obj,
|
||||
@ -73,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (!OperationInProgress.Value)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
|
||||
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && DefaultOrigin != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
Vector2 actualOrigin = origin ?? DefaultOrigin.Value;
|
||||
|
||||
foreach (var ho in objectsInRotation)
|
||||
{
|
||||
@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
objectsInRotation = null;
|
||||
originalPositions = null;
|
||||
originalPathControlPointPositions = null;
|
||||
defaultOrigin = null;
|
||||
DefaultOrigin = null;
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
|
@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
||||
private Vector2? defaultOrigin;
|
||||
private List<Vector2>? originalConvexHull;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
@ -83,10 +84,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
|
||||
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
|
||||
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
|
||||
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
if (!OperationInProgress.Value)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
@ -94,23 +98,22 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
scale = clampScaleToAdjustAxis(scale, adjustAxis);
|
||||
|
||||
// for the time being, allow resizing of slider paths only if the slider is
|
||||
// the only hit object selected. with a group selection, it's likely the user
|
||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||
{
|
||||
var originalInfo = objectsInScale[slider];
|
||||
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
|
||||
scaleSlider(slider, scale, actualOrigin, objectsInScale[slider], axisRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin);
|
||||
scale = ClampScaleToPlayfieldBounds(scale, actualOrigin, adjustAxis, axisRotation);
|
||||
|
||||
foreach (var (ho, originalState) in objectsInScale)
|
||||
{
|
||||
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
|
||||
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation);
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,21 +137,45 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
.Where(h => h is not Spinner);
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
|
||||
private Vector2 clampScaleToAdjustAxis(Vector2 scale, Axes adjustAxis)
|
||||
{
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.Y:
|
||||
scale.X = 1;
|
||||
break;
|
||||
|
||||
case Axes.X:
|
||||
scale.Y = 1;
|
||||
break;
|
||||
|
||||
case Axes.None:
|
||||
scale = Vector2.One;
|
||||
break;
|
||||
}
|
||||
|
||||
return scale;
|
||||
}
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2 origin, OriginalHitObjectState originalInfo, float axisRotation = 0)
|
||||
{
|
||||
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||
|
||||
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
|
||||
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
|
||||
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation);
|
||||
slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i];
|
||||
}
|
||||
|
||||
// Snap the slider's length to the current beat divisor
|
||||
// to calculate the final resulting duration / bounding box before the final checks.
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
@ -157,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
|
||||
slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i];
|
||||
|
||||
slider.Position = originalInfo.Position;
|
||||
|
||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||
slider.SnapTo(snapProvider);
|
||||
@ -176,11 +205,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// </summary>
|
||||
/// <param name="origin">The origin from which the scale operation is performed</param>
|
||||
/// <param name="scale">The scale to be clamped</param>
|
||||
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||
/// <param name="axisRotation">The rotation of the axes in degrees</param>
|
||||
/// <returns>The clamped scale vector</returns>
|
||||
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null)
|
||||
public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||
if (objectsInScale == null)
|
||||
if (objectsInScale == null || adjustAxis == Axes.None)
|
||||
return scale;
|
||||
|
||||
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||
@ -188,24 +219,60 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||
origin = slider.Position;
|
||||
|
||||
float cos = MathF.Cos(float.DegreesToRadians(-axisRotation));
|
||||
float sin = MathF.Sin(float.DegreesToRadians(-axisRotation));
|
||||
scale = clampScaleToAdjustAxis(scale, adjustAxis);
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
var selectionQuad = OriginalSurroundingQuad.Value;
|
||||
IEnumerable<Vector2> points;
|
||||
|
||||
var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin);
|
||||
var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin);
|
||||
if (axisRotation == 0)
|
||||
{
|
||||
var selectionQuad = OriginalSurroundingQuad.Value;
|
||||
points = new[]
|
||||
{
|
||||
selectionQuad.TopLeft,
|
||||
selectionQuad.TopRight,
|
||||
selectionQuad.BottomLeft,
|
||||
selectionQuad.BottomRight
|
||||
};
|
||||
}
|
||||
else
|
||||
points = originalConvexHull!;
|
||||
|
||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0))
|
||||
scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0))
|
||||
scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0))
|
||||
scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0))
|
||||
scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
|
||||
foreach (var point in points)
|
||||
{
|
||||
scale = clampToBound(scale, point, Vector2.Zero);
|
||||
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
|
||||
}
|
||||
|
||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
|
||||
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
|
||||
|
||||
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
|
||||
{
|
||||
p -= actualOrigin;
|
||||
bound -= actualOrigin;
|
||||
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
||||
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
||||
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.X:
|
||||
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
|
||||
break;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -19,16 +20,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
private readonly SelectionRotationHandler rotationHandler;
|
||||
|
||||
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
|
||||
private readonly OsuGridToolboxGroup gridToolbox;
|
||||
|
||||
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.GridCentre));
|
||||
|
||||
private SliderWithTextBoxInput<float> angleInput = null!;
|
||||
private EditorRadioButtonCollection rotationOrigin = null!;
|
||||
|
||||
private RadioButton selectionCentreButton = null!;
|
||||
|
||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
|
||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox)
|
||||
{
|
||||
this.rotationHandler = rotationHandler;
|
||||
this.gridToolbox = gridToolbox;
|
||||
|
||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||
}
|
||||
@ -58,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = new[]
|
||||
{
|
||||
new RadioButton("Grid centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||
new RadioButton("Playfield centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
@ -93,10 +100,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
rotationInfo.BindValueChanged(rotation =>
|
||||
{
|
||||
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
|
||||
rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue));
|
||||
});
|
||||
}
|
||||
|
||||
private Vector2? getOriginPosition(PreciseRotationInfo rotation) =>
|
||||
rotation.Origin switch
|
||||
{
|
||||
RotationOrigin.GridCentre => gridToolbox.StartPosition.Value,
|
||||
RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2,
|
||||
RotationOrigin.SelectionCentre => null,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(rotation))
|
||||
};
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
@ -114,6 +130,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public enum RotationOrigin
|
||||
{
|
||||
GridCentre,
|
||||
PlayfieldCentre,
|
||||
SelectionCentre
|
||||
}
|
||||
|
@ -10,7 +10,10 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
using osuTK;
|
||||
|
||||
@ -20,28 +23,36 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
private readonly OsuSelectionScaleHandler scaleHandler;
|
||||
|
||||
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true));
|
||||
private readonly OsuGridToolboxGroup gridToolbox;
|
||||
|
||||
private readonly Bindable<PreciseScaleInfo> scaleInfo = new Bindable<PreciseScaleInfo>(new PreciseScaleInfo(1, ScaleOrigin.GridCentre, true, true));
|
||||
|
||||
private SliderWithTextBoxInput<float> scaleInput = null!;
|
||||
private BindableNumber<float> scaleInputBindable = null!;
|
||||
private EditorRadioButtonCollection scaleOrigin = null!;
|
||||
|
||||
private RadioButton gridCentreButton = null!;
|
||||
private RadioButton playfieldCentreButton = null!;
|
||||
private RadioButton selectionCentreButton = null!;
|
||||
|
||||
private OsuCheckbox xCheckBox = null!;
|
||||
private OsuCheckbox yCheckBox = null!;
|
||||
|
||||
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler)
|
||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||
|
||||
public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox)
|
||||
{
|
||||
this.scaleHandler = scaleHandler;
|
||||
this.gridToolbox = gridToolbox;
|
||||
|
||||
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 220,
|
||||
@ -66,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Items = new[]
|
||||
{
|
||||
gridCentreButton = new RadioButton("Grid centre",
|
||||
() => setOrigin(ScaleOrigin.GridCentre),
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }),
|
||||
playfieldCentreButton = new RadioButton("Playfield centre",
|
||||
() => setOrigin(ScaleOrigin.PlayfieldCentre),
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
@ -97,6 +111,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
},
|
||||
}
|
||||
};
|
||||
gridCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
gridCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to grid centre." : string.Empty;
|
||||
};
|
||||
playfieldCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty;
|
||||
@ -123,19 +141,20 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value);
|
||||
playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled;
|
||||
gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled;
|
||||
|
||||
scaleOrigin.Items.First(b => !b.Selected.Disabled).Select();
|
||||
|
||||
scaleInfo.BindValueChanged(scale =>
|
||||
{
|
||||
var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1);
|
||||
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue));
|
||||
var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale);
|
||||
scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue));
|
||||
});
|
||||
}
|
||||
|
||||
private void updateAxisCheckBoxesEnabled()
|
||||
{
|
||||
if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre)
|
||||
if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre)
|
||||
{
|
||||
toggleAxisAvailable(xCheckBox.Current, true);
|
||||
toggleAxisAvailable(yCheckBox.Current, true);
|
||||
@ -162,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return;
|
||||
|
||||
const float max_scale = 10;
|
||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value));
|
||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||
|
||||
if (!scaleInfo.Value.XAxis)
|
||||
scale.X = max_scale;
|
||||
@ -179,7 +198,30 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
updateAxisCheckBoxesEnabled();
|
||||
}
|
||||
|
||||
private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null;
|
||||
private Vector2? getOriginPosition(PreciseScaleInfo scale)
|
||||
{
|
||||
switch (scale.Origin)
|
||||
{
|
||||
case ScaleOrigin.GridCentre:
|
||||
return gridToolbox.StartPosition.Value;
|
||||
|
||||
case ScaleOrigin.PlayfieldCentre:
|
||||
return OsuPlayfield.BASE_SIZE / 2;
|
||||
|
||||
case ScaleOrigin.SelectionCentre:
|
||||
if (selectedItems.Count == 1 && selectedItems.First() is Slider slider)
|
||||
return slider.Position;
|
||||
|
||||
return null;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(scale));
|
||||
}
|
||||
}
|
||||
|
||||
private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y;
|
||||
|
||||
private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0;
|
||||
|
||||
private void setAxis(bool x, bool y)
|
||||
{
|
||||
@ -204,6 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public enum ScaleOrigin
|
||||
{
|
||||
GridCentre,
|
||||
PlayfieldCentre,
|
||||
SelectionCentre
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class SliderCompositionTool : HitObjectCompositionTool
|
||||
public class SliderCompositionTool : CompositionTool
|
||||
{
|
||||
public SliderCompositionTool()
|
||||
: base(nameof(Slider))
|
||||
@ -26,6 +26,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class SpinnerCompositionTool : HitObjectCompositionTool
|
||||
public class SpinnerCompositionTool : CompositionTool
|
||||
{
|
||||
public SpinnerCompositionTool()
|
||||
: base(nameof(Spinner))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||
public OsuSelectionScaleHandler ScaleHandler { get; init; } = null!;
|
||||
|
||||
public OsuGridToolboxGroup GridToolbox { get; init; } = null!;
|
||||
|
||||
public TransformToolboxGroup()
|
||||
: base("transform")
|
||||
{
|
||||
@ -44,10 +46,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
rotateButton = new EditorToolButton("Rotate",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||
() => new PreciseRotationPopover(RotationHandler)),
|
||||
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
||||
scaleButton = new EditorToolButton("Scale",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||
() => new PreciseScalePopover(ScaleHandler))
|
||||
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -91,20 +91,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
||||
drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject;
|
||||
}
|
||||
else
|
||||
applyDim(piece);
|
||||
}
|
||||
|
||||
void applyDim(Drawable piece)
|
||||
{
|
||||
piece.FadeColour(new Color4(195, 195, 195, 255));
|
||||
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||
piece.FadeColour(Color4.White, 100);
|
||||
// but at the end apply the transforms now regardless of whether this is a DHO or not.
|
||||
// the above is just to ensure they don't get overwritten later.
|
||||
applyDim(piece);
|
||||
}
|
||||
|
||||
void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
|
||||
}
|
||||
|
||||
protected override void ClearNestedHitObjects()
|
||||
{
|
||||
base.ClearNestedHitObjects();
|
||||
|
||||
// any dimmable pieces that are DHOs will be pooled separately.
|
||||
// `applyDimToDrawableHitObject` is a closure that implicitly captures `this`,
|
||||
// and because of separate pooling of parent and child objects, there is no guarantee that the pieces will be associated with `this` again on re-use.
|
||||
// therefore, clean up the subscription here to avoid crosstalk.
|
||||
// not doing so can result in the callback attempting to read things from `this` when it is in a completely bogus state (not in use or similar).
|
||||
foreach (var piece in DimmablePieces.OfType<DrawableHitObject>())
|
||||
piece.ApplyCustomUpdateState -= applyDimToDrawableHitObject;
|
||||
}
|
||||
|
||||
private void applyDim(Drawable piece)
|
||||
{
|
||||
piece.FadeColour(new Color4(195, 195, 195, 255));
|
||||
using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
|
||||
piece.FadeColour(Color4.White, 100);
|
||||
}
|
||||
|
||||
private void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho);
|
||||
|
||||
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
|
||||
|
||||
private OsuInputManager osuActionInputManager;
|
||||
|
@ -1,11 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Input.Handlers;
|
||||
@ -25,18 +26,38 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
public partial class DrawableOsuRuleset : DrawableRuleset<OsuHitObject>
|
||||
{
|
||||
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
|
||||
private Bindable<bool>? cursorHideEnabled;
|
||||
|
||||
public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;
|
||||
|
||||
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
|
||||
|
||||
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
|
||||
|
||||
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
}
|
||||
|
||||
public override DrawableHitObject<OsuHitObject> CreateDrawableRepresentation(OsuHitObject h) => null;
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ReplayPlayer? replayPlayer)
|
||||
{
|
||||
if (replayPlayer != null)
|
||||
{
|
||||
ReplayAnalysisOverlay analysisOverlay;
|
||||
PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(replayPlayer.Score.Replay));
|
||||
Overlays.Add(analysisOverlay.CreateProxy().With(p => p.Depth = float.NegativeInfinity));
|
||||
replayPlayer.AddSettings(new ReplayAnalysisSettings(Config));
|
||||
|
||||
cursorHideEnabled = Config.GetBindable<bool>(OsuRulesetSetting.ReplayCursorHideEnabled);
|
||||
|
||||
// I have little faith in this working (other things touch cursor visibility) but haven't broken it yet.
|
||||
// Let's wait for someone to report an issue before spending too much time on it.
|
||||
cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true);
|
||||
}
|
||||
}
|
||||
|
||||
public override DrawableHitObject<OsuHitObject>? CreateDrawableRepresentation(OsuHitObject h) => null;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor
|
||||
|
||||
|
@ -0,0 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||
{
|
||||
public partial class AnalysisFrameEntry : LifetimeEntry
|
||||
{
|
||||
public OsuAction[] Action { get; }
|
||||
|
||||
public Vector2 Position { get; }
|
||||
|
||||
public AnalysisFrameEntry(double time, double displayLength, Vector2 position, params OsuAction[] action)
|
||||
{
|
||||
LifetimeStart = time;
|
||||
LifetimeEnd = time + displayLength;
|
||||
Position = position;
|
||||
Action = action;
|
||||
}
|
||||
}
|
||||
}
|
28
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs
Normal file
28
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||
{
|
||||
public abstract partial class AnalysisMarker : PoolableDrawableWithLifetime<AnalysisFrameEntry>
|
||||
{
|
||||
[Resolved]
|
||||
protected OsuColour Colours { get; private set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
protected override void OnApply(AnalysisFrameEntry entry)
|
||||
{
|
||||
Position = entry.Position;
|
||||
Depth = -(float)entry.LifetimeEnd;
|
||||
}
|
||||
}
|
||||
}
|
88
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs
Normal file
88
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||
{
|
||||
/// <summary>
|
||||
/// A marker which shows one click, with visuals focusing on the button which was clicked and the precise location of the click.
|
||||
/// </summary>
|
||||
public partial class ClickMarker : AnalysisMarker
|
||||
{
|
||||
private CircularProgress leftClickDisplay = null!;
|
||||
private CircularProgress rightClickDisplay = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.125f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = Colours.Gray5,
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colours.Gray5,
|
||||
Masking = true,
|
||||
BorderThickness = 2.2f,
|
||||
BorderColour = Color4.White,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0,
|
||||
},
|
||||
},
|
||||
leftClickDisplay = new CircularProgress
|
||||
{
|
||||
Colour = Colours.Yellow,
|
||||
Size = new Vector2(0.95f),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreRight,
|
||||
Rotation = 180,
|
||||
Progress = 0.5f,
|
||||
InnerRadius = 0.18f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
rightClickDisplay = new CircularProgress
|
||||
{
|
||||
Colour = Colours.Yellow,
|
||||
Size = new Vector2(0.95f),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Progress = 0.5f,
|
||||
InnerRadius = 0.18f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
};
|
||||
|
||||
Size = new Vector2(16);
|
||||
}
|
||||
|
||||
protected override void OnApply(AnalysisFrameEntry entry)
|
||||
{
|
||||
base.OnApply(entry);
|
||||
|
||||
leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0;
|
||||
rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||
{
|
||||
public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer<AnalysisFrameEntry, AnalysisMarker>
|
||||
{
|
||||
private readonly DrawablePool<ClickMarker> clickMarkerPool;
|
||||
|
||||
public ClickMarkerContainer()
|
||||
{
|
||||
AddInternal(clickMarkerPool = new DrawablePool<ClickMarker>(30));
|
||||
}
|
||||
|
||||
protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry));
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics.Lines;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||
{
|
||||
public partial class CursorPathContainer : Path
|
||||
{
|
||||
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
|
||||
private readonly SortedSet<AnalysisFrameEntry> aliveEntries = new SortedSet<AnalysisFrameEntry>(new AimLinePointComparator());
|
||||
|
||||
public CursorPathContainer()
|
||||
{
|
||||
lifetimeManager.EntryBecameAlive += entryBecameAlive;
|
||||
lifetimeManager.EntryBecameDead += entryBecameDead;
|
||||
|
||||
PathRadius = 0.5f;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Colour = colours.Pink2;
|
||||
BackgroundColour = colours.Pink2.Opacity(0);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
lifetimeManager.Update(Time.Current);
|
||||
}
|
||||
|
||||
public void Add(AnalysisFrameEntry entry) => lifetimeManager.AddEntry(entry);
|
||||
|
||||
private void entryBecameAlive(LifetimeEntry entry)
|
||||
{
|
||||
aliveEntries.Add((AnalysisFrameEntry)entry);
|
||||
updateVertices();
|
||||
}
|
||||
|
||||
private void entryBecameDead(LifetimeEntry entry)
|
||||
{
|
||||
aliveEntries.Remove((AnalysisFrameEntry)entry);
|
||||
updateVertices();
|
||||
}
|
||||
|
||||
private void updateVertices()
|
||||
{
|
||||
ClearVertices();
|
||||
|
||||
Vector2 min = Vector2.Zero;
|
||||
|
||||
foreach (var entry in aliveEntries)
|
||||
{
|
||||
AddVertex(entry.Position);
|
||||
if (entry.Position.X < min.X)
|
||||
min.X = entry.Position.X;
|
||||
|
||||
if (entry.Position.Y < min.Y)
|
||||
min.Y = entry.Position.Y;
|
||||
}
|
||||
|
||||
Position = min;
|
||||
}
|
||||
|
||||
private sealed class AimLinePointComparator : IComparer<AnalysisFrameEntry>
|
||||
{
|
||||
public int Compare(AnalysisFrameEntry? x, AnalysisFrameEntry? y)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(x);
|
||||
ArgumentNullException.ThrowIfNull(y);
|
||||
|
||||
return x.LifetimeStart.CompareTo(y.LifetimeStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs
Normal file
69
osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||
{
|
||||
/// <summary>
|
||||
/// A marker which shows one movement frame, include any buttons which are pressed.
|
||||
/// </summary>
|
||||
public partial class FrameMarker : AnalysisMarker
|
||||
{
|
||||
private CircularProgress leftClickDisplay = null!;
|
||||
private CircularProgress rightClickDisplay = null!;
|
||||
private Circle mainCircle = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
mainCircle = new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colours.Pink2,
|
||||
},
|
||||
leftClickDisplay = new CircularProgress
|
||||
{
|
||||
Colour = Colours.Yellow,
|
||||
Size = new Vector2(0.8f),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreRight,
|
||||
Rotation = 180,
|
||||
Progress = 0.5f,
|
||||
InnerRadius = 0.5f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
rightClickDisplay = new CircularProgress
|
||||
{
|
||||
Colour = Colours.Yellow,
|
||||
Size = new Vector2(0.8f),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Progress = 0.5f,
|
||||
InnerRadius = 0.5f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnApply(AnalysisFrameEntry entry)
|
||||
{
|
||||
base.OnApply(entry);
|
||||
Size = new Vector2(entry.Action.Any() ? 4 : 2.5f);
|
||||
|
||||
mainCircle.Colour = entry.Action.Any() ? Colours.Gray4 : Colours.Pink2;
|
||||
|
||||
leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0;
|
||||
rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
|
||||
{
|
||||
public partial class FrameMarkerContainer : PooledDrawableWithLifetimeContainer<AnalysisFrameEntry, AnalysisMarker>
|
||||
{
|
||||
private readonly DrawablePool<FrameMarker> pool;
|
||||
|
||||
public FrameMarkerContainer()
|
||||
{
|
||||
AddInternal(pool = new DrawablePool<FrameMarker>(80));
|
||||
}
|
||||
|
||||
protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry));
|
||||
}
|
||||
}
|
128
osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs
Normal file
128
osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI.ReplayAnalysis;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
public partial class ReplayAnalysisOverlay : CompositeDrawable
|
||||
{
|
||||
private BindableBool showClickMarkers { get; } = new BindableBool();
|
||||
private BindableBool showFrameMarkers { get; } = new BindableBool();
|
||||
private BindableBool showCursorPath { get; } = new BindableBool();
|
||||
private BindableInt displayLength { get; } = new BindableInt();
|
||||
|
||||
protected ClickMarkerContainer? ClickMarkers;
|
||||
protected FrameMarkerContainer? FrameMarkers;
|
||||
protected CursorPathContainer? CursorPath;
|
||||
|
||||
private readonly Replay replay;
|
||||
|
||||
public ReplayAnalysisOverlay(Replay replay)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
this.replay = replay;
|
||||
}
|
||||
|
||||
private bool requireDisplay => showClickMarkers.Value || showFrameMarkers.Value || showCursorPath.Value;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuRulesetConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, showClickMarkers);
|
||||
config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, showFrameMarkers);
|
||||
config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, showCursorPath);
|
||||
config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, displayLength);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
displayLength.BindValueChanged(_ =>
|
||||
{
|
||||
// Need to fully reload to make this work.
|
||||
loaded.Invalidate();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private readonly Cached loaded = new Cached();
|
||||
|
||||
private CancellationTokenSource? generationCancellationSource;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (requireDisplay)
|
||||
initialise();
|
||||
|
||||
if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0;
|
||||
if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0;
|
||||
if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0;
|
||||
}
|
||||
|
||||
private void initialise()
|
||||
{
|
||||
if (loaded.IsValid)
|
||||
return;
|
||||
|
||||
loaded.Validate();
|
||||
|
||||
generationCancellationSource?.Cancel();
|
||||
generationCancellationSource = new CancellationTokenSource();
|
||||
|
||||
// It's faster to reinitialise the whole drawable stack than use `Clear` on `PooledDrawableWithLifetimeContainer`
|
||||
var newDrawables = new Drawable[]
|
||||
{
|
||||
CursorPath = new CursorPathContainer(),
|
||||
ClickMarkers = new ClickMarkerContainer(),
|
||||
FrameMarkers = new FrameMarkerContainer(),
|
||||
};
|
||||
|
||||
bool leftHeld = false;
|
||||
bool rightHeld = false;
|
||||
|
||||
// This should probably be async as well, but it's a bit of a pain to debounce and everything.
|
||||
// Let's address concerns when they are raised.
|
||||
foreach (var frame in replay.Frames)
|
||||
{
|
||||
var osuFrame = (OsuReplayFrame)frame;
|
||||
|
||||
bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton);
|
||||
bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton);
|
||||
|
||||
if (leftHeld && !leftButton)
|
||||
leftHeld = false;
|
||||
else if (!leftHeld && leftButton)
|
||||
{
|
||||
leftHeld = true;
|
||||
ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.LeftButton));
|
||||
}
|
||||
|
||||
if (rightHeld && !rightButton)
|
||||
rightHeld = false;
|
||||
else if (!rightHeld && rightButton)
|
||||
{
|
||||
rightHeld = true;
|
||||
ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.RightButton));
|
||||
}
|
||||
|
||||
FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray()));
|
||||
CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position));
|
||||
}
|
||||
|
||||
LoadComponentsAsync(newDrawables, drawables => InternalChildrenEnumerable = drawables, generationCancellationSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
55
osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs
Normal file
55
osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
public partial class ReplayAnalysisSettings : PlayerSettingsGroup
|
||||
{
|
||||
private readonly OsuRulesetConfigManager config;
|
||||
|
||||
[SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))]
|
||||
public BindableBool ShowClickMarkers { get; } = new BindableBool();
|
||||
|
||||
[SettingSource("Show frame markers", SettingControlType = typeof(PlayerCheckbox))]
|
||||
public BindableBool ShowAimMarkers { get; } = new BindableBool();
|
||||
|
||||
[SettingSource("Show cursor path", SettingControlType = typeof(PlayerCheckbox))]
|
||||
public BindableBool ShowCursorPath { get; } = new BindableBool();
|
||||
|
||||
[SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))]
|
||||
public BindableBool HideSkinCursor { get; } = new BindableBool();
|
||||
|
||||
[SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar<int>))]
|
||||
public BindableInt DisplayLength { get; } = new BindableInt
|
||||
{
|
||||
MinValue = 200,
|
||||
MaxValue = 2000,
|
||||
Default = 800,
|
||||
Precision = 200,
|
||||
};
|
||||
|
||||
public ReplayAnalysisSettings(OsuRulesetConfigManager config)
|
||||
: base("Analysis Settings")
|
||||
{
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddRange(this.CreateSettingsControls());
|
||||
|
||||
config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, ShowClickMarkers);
|
||||
config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers);
|
||||
config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath);
|
||||
config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor);
|
||||
config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, DisplayLength);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Argon;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class VolumeAwareHitSampleInfoTest
|
||||
{
|
||||
[Test]
|
||||
public void TestVolumeAwareHitSampleInfoIsNotEqualToItsUnderlyingSample(
|
||||
[Values(HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP)]
|
||||
string sample,
|
||||
[Values(HitSampleInfo.BANK_NORMAL, HitSampleInfo.BANK_SOFT)]
|
||||
string bank,
|
||||
[Values(30, 70, 100)] int volume)
|
||||
{
|
||||
var underlyingSample = new HitSampleInfo(sample, bank, volume: volume);
|
||||
var volumeAwareSample = new VolumeAwareHitSampleInfo(underlyingSample);
|
||||
|
||||
Assert.That(underlyingSample, Is.Not.EqualTo(volumeAwareSample));
|
||||
}
|
||||
}
|
||||
}
|
@ -14,19 +14,16 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 1.35;
|
||||
|
||||
private const double final_multiplier = 0.0625;
|
||||
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
|
||||
private const double colour_skill_multiplier = 0.375 * final_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
|
||||
private const double difficulty_multiplier = 0.084375;
|
||||
private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier;
|
||||
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
|
||||
public override int Version => 20221107;
|
||||
|
||||
@ -83,11 +80,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
||||
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier;
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
|
||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier;
|
||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
|
||||
double starRating = rescale(combinedRating * 1.4);
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
@ -102,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
ColourDifficulty = colourRating,
|
||||
PeakDifficulty = combinedRating,
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
return attributes;
|
||||
|
@ -10,7 +10,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
{
|
||||
public partial class HitPlacementBlueprint : PlacementBlueprint
|
||||
public partial class HitPlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
private readonly HitPiece piece;
|
||||
|
||||
|
@ -17,7 +17,7 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
{
|
||||
public partial class TaikoSpanPlacementBlueprint : PlacementBlueprint
|
||||
public partial class TaikoSpanPlacementBlueprint : HitObjectPlacementBlueprint
|
||||
{
|
||||
private readonly HitPiece headPiece;
|
||||
private readonly HitPiece tailPiece;
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public class DrumRollCompositionTool : HitObjectCompositionTool
|
||||
public class DrumRollCompositionTool : CompositionTool
|
||||
{
|
||||
public DrumRollCompositionTool()
|
||||
: base(nameof(DrumRoll))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public class HitCompositionTool : HitObjectCompositionTool
|
||||
public class HitCompositionTool : CompositionTool
|
||||
{
|
||||
public HitCompositionTool()
|
||||
: base(nameof(Hit))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public class SwellCompositionTool : HitObjectCompositionTool
|
||||
public class SwellCompositionTool : CompositionTool
|
||||
{
|
||||
public SwellCompositionTool()
|
||||
: base(nameof(Swell))
|
||||
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
|
||||
|
||||
public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
|
||||
public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
protected override IReadOnlyList<CompositionTool> CompositionTools => new CompositionTool[]
|
||||
{
|
||||
new HitCompositionTool(),
|
||||
new DrumRollCompositionTool(),
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
|
||||
@ -48,5 +49,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
return originalBank;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(HitSampleInfo? other) => other is VolumeAwareHitSampleInfo && base.Equals(other);
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This override attempts to match the <see cref="Equals"/> override above, but in theory it is not strictly necessary.
|
||||
/// Recall that <see cref="GetHashCode"/> <a href="https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-8.0#notes-to-inheritors">must meet the following requirements</a>:
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// "If two objects compare as equal, the <see cref="GetHashCode"/> method for each object must return the same value.
|
||||
/// However, if two objects do not compare as equal, <see cref="GetHashCode"/> methods for the two objects do not have to return different values."
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Making this override combine the value generated by the base <see cref="GetHashCode"/> implementation with a constant means
|
||||
/// that <see cref="HitSampleInfo"/> and <see cref="VolumeAwareHitSampleInfo"/> instances which have the same values of their members
|
||||
/// will not have equal hash codes, which is slightly more efficient when these objects are used as dictionary keys.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), 1);
|
||||
}
|
||||
}
|
||||
|
@ -633,11 +633,15 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
new object[] { "0", DateTimeOffset.Now, false },
|
||||
new object[] { "false", DateTimeOffset.MinValue, true },
|
||||
new object[] { "false", DateTimeOffset.Now, false },
|
||||
new object[] { "no", DateTimeOffset.MinValue, true },
|
||||
new object[] { "no", DateTimeOffset.Now, false },
|
||||
|
||||
new object[] { "1", DateTimeOffset.MinValue, false },
|
||||
new object[] { "1", DateTimeOffset.Now, true },
|
||||
new object[] { "true", DateTimeOffset.MinValue, false },
|
||||
new object[] { "true", DateTimeOffset.Now, true },
|
||||
new object[] { "yes", DateTimeOffset.MinValue, false },
|
||||
new object[] { "yes", DateTimeOffset.Now, true },
|
||||
};
|
||||
|
||||
[Test]
|
||||
|
52
osu.Game.Tests/Utils/BindableValueAccessorTest.cs
Normal file
52
osu.Game.Tests/Utils/BindableValueAccessorTest.cs
Normal 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 NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Utils
|
||||
{
|
||||
[TestFixture]
|
||||
public class BindableValueAccessorTest
|
||||
{
|
||||
[Test]
|
||||
public void GetValue()
|
||||
{
|
||||
const int value = 1337;
|
||||
|
||||
BindableInt bindable = new BindableInt(value);
|
||||
Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetValue()
|
||||
{
|
||||
const int value = 1337;
|
||||
|
||||
BindableInt bindable = new BindableInt();
|
||||
BindableValueAccessor.SetValue(bindable, value);
|
||||
|
||||
Assert.That(bindable.Value, Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetInvalidBindable()
|
||||
{
|
||||
BindableList<object> list = new BindableList<object>();
|
||||
Assert.That(BindableValueAccessor.GetValue(list), Is.EqualTo(list));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetInvalidBindable()
|
||||
{
|
||||
const int value = 1337;
|
||||
|
||||
BindableList<int> list = new BindableList<int> { value };
|
||||
BindableValueAccessor.SetValue(list, 2);
|
||||
|
||||
Assert.That(list, Has.Exactly(1).Items);
|
||||
Assert.That(list[0], Is.EqualTo(value));
|
||||
}
|
||||
}
|
||||
}
|
51
osu.Game.Tests/Utils/GeometryUtilsTest.cs
Normal file
51
osu.Game.Tests/Utils/GeometryUtilsTest.cs
Normal file
@ -0,0 +1,51 @@
|
||||
// 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.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Utils
|
||||
{
|
||||
[TestFixture]
|
||||
public class GeometryUtilsTest
|
||||
{
|
||||
[TestCase(new int[] { }, new int[] { })]
|
||||
[TestCase(new[] { 0, 0 }, new[] { 0, 0 })]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, new[] { 0, 0, 4, 10, 2, -1 })]
|
||||
public void TestConvexHull(int[] values, int[] expected)
|
||||
{
|
||||
var points = new Vector2[values.Length / 2];
|
||||
for (int i = 0; i < values.Length; i += 2)
|
||||
points[i / 2] = new Vector2(values[i], values[i + 1]);
|
||||
|
||||
var expectedPoints = new Vector2[expected.Length / 2];
|
||||
for (int i = 0; i < expected.Length; i += 2)
|
||||
expectedPoints[i / 2] = new Vector2(expected[i], expected[i + 1]);
|
||||
|
||||
var hull = GeometryUtils.GetConvexHull(points);
|
||||
|
||||
Assert.That(hull, Is.EquivalentTo(expectedPoints));
|
||||
}
|
||||
|
||||
[TestCase(new int[] { }, 0, 0, 0)]
|
||||
[TestCase(new[] { 0, 0 }, 0, 0, 0)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, 1, 0, 1)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, 1, 0, 1)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, 3, 4.5f, 5.5901699f)]
|
||||
public void TestMinimumEnclosingCircle(int[] values, float x, float y, float r)
|
||||
{
|
||||
var points = new Vector2[values.Length / 2];
|
||||
for (int i = 0; i < values.Length; i += 2)
|
||||
points[i / 2] = new Vector2(values[i], values[i + 1]);
|
||||
|
||||
(var centre, float radius) = GeometryUtils.MinimumEnclosingCircle(points);
|
||||
|
||||
Assert.That(centre.X, Is.EqualTo(x).Within(0.0001));
|
||||
Assert.That(centre.Y, Is.EqualTo(y).Within(0.0001));
|
||||
Assert.That(radius, Is.EqualTo(r).Within(0.0001));
|
||||
}
|
||||
}
|
||||
}
|
@ -84,6 +84,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
targetContainer = getTargetContainer();
|
||||
initialRotation = targetContainer!.Rotation;
|
||||
DefaultOrigin = ToLocalSpace(targetContainer.ToScreenSpace(Vector2.Zero));
|
||||
|
||||
base.Begin();
|
||||
}
|
||||
@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
@ -36,6 +37,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private ContextMenuContainer contextMenuContainer
|
||||
=> Editor.ChildrenOfType<ContextMenuContainer>().First();
|
||||
|
||||
private SelectionBoxScaleHandle getScaleHandle(Anchor anchor)
|
||||
=> Editor.ChildrenOfType<SelectionBoxScaleHandle>().First(it => it.Anchor == anchor);
|
||||
|
||||
private void moveMouseToObject(Func<HitObject> targetFunc)
|
||||
{
|
||||
AddStep("move mouse to object", () =>
|
||||
@ -215,6 +219,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiSelectWithDragBox()
|
||||
{
|
||||
var addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100 },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||
new HitCircle { StartTime = 300, Position = new Vector2(512, 0) },
|
||||
new HitCircle { StartTime = 400, Position = new Vector2(412, 100) },
|
||||
};
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
|
||||
|
||||
AddStep("start dragging", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopLeft - new Vector2(5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
|
||||
|
||||
AddStep("start dragging with control", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
|
||||
|
||||
AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4));
|
||||
|
||||
AddStep("start dragging without control", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNearestSelection()
|
||||
{
|
||||
@ -519,5 +568,137 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShiftModifierMaintainsAspectRatio()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
float aspectRatioBeforeDrag = 0;
|
||||
|
||||
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
aspectRatioBeforeDrag = getAspectRatio();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAltModifierScalesAroundCenter()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
Vector2 centerBeforeDrag = Vector2.Zero;
|
||||
|
||||
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
centerBeforeDrag = getCenter();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
|
||||
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShiftAndAltModifierKeys()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
float aspectRatioBeforeDrag = 0;
|
||||
|
||||
Vector2 centerBeforeDrag = Vector2.Zero;
|
||||
|
||||
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
|
||||
|
||||
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
aspectRatioBeforeDrag = getAspectRatio();
|
||||
centerBeforeDrag = getCenter();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
|
||||
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@ -136,6 +137,59 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCalibrationFromNonZeroWithImmediateReferenceScore()
|
||||
{
|
||||
const double average_error = -4.5;
|
||||
const double initial_offset = -2;
|
||||
|
||||
AddStep("Set beatmap offset non-neutral", () => Realm.Write(r =>
|
||||
{
|
||||
r.Add(new BeatmapInfo
|
||||
{
|
||||
ID = Beatmap.Value.BeatmapInfo.ID,
|
||||
Ruleset = Beatmap.Value.BeatmapInfo.Ruleset,
|
||||
UserSettings =
|
||||
{
|
||||
Offset = initial_offset,
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
AddStep("Create control with preloaded reference score", () =>
|
||||
{
|
||||
Child = new PlayerSettingsGroup("Some settings")
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
offsetControl = new BeatmapOffsetControl
|
||||
{
|
||||
ReferenceScore =
|
||||
{
|
||||
Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
|
||||
AddAssert("Offset is adjusted", () => offsetControl.Current.Value, () => Is.EqualTo(initial_offset - average_error));
|
||||
|
||||
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
|
||||
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
|
||||
AddStep("Clean up beatmap", () => Realm.Write(r => r.RemoveAll<BeatmapInfo>()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCalibrationNoChange()
|
||||
{
|
||||
|
@ -43,6 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset()))
|
||||
{
|
||||
ProcessCustomClock = false,
|
||||
BreakTracker = breakTracker,
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -57,9 +58,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestShowBreaks()
|
||||
{
|
||||
setClock(false);
|
||||
|
||||
addShowBreakStep(2);
|
||||
addShowBreakStep(5);
|
||||
addShowBreakStep(15);
|
||||
}
|
||||
@ -124,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep($"show '{seconds}s' break", () =>
|
||||
{
|
||||
breakOverlay.Breaks = breakTracker.Breaks = new List<BreakPeriod>
|
||||
breakTracker.Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000)
|
||||
};
|
||||
@ -138,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private void loadBreaksStep(string breakDescription, IReadOnlyList<BreakPeriod> breaks)
|
||||
{
|
||||
AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks);
|
||||
AddStep($"load {breakDescription}", () => breakTracker.Breaks = breaks);
|
||||
seekAndAssertBreak("seek back to 0", 0, false);
|
||||
}
|
||||
|
||||
@ -184,6 +182,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
public TestBreakTracker()
|
||||
: base(0, new ScoreProcessor(new OsuRuleset()))
|
||||
{
|
||||
FramedManualClock = new FramedClock(manualClock = new ManualClock());
|
||||
ProcessCustomClock = false;
|
||||
|
@ -8,7 +8,9 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
@ -26,6 +28,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -177,6 +180,30 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyFailStillImports()
|
||||
{
|
||||
prepareTestAPI(true);
|
||||
|
||||
createPlayerTest(true);
|
||||
|
||||
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
|
||||
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
|
||||
AddStep("attempt import", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Player.ChildrenOfType<SaveFailedScoreButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("wait for import to start", () => Player.ScoreImportStarted);
|
||||
AddStep("allow import", () => Player.AllowImportCompletion.Release());
|
||||
|
||||
AddUntilStep("import completed", () => Player.ImportedScore, () => Is.Not.Null);
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore, () => Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSubmissionOnFail()
|
||||
{
|
||||
@ -378,6 +405,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public SemaphoreSlim AllowImportCompletion { get; }
|
||||
public Score ImportedScore { get; private set; }
|
||||
|
||||
public new FailOverlay FailOverlay => base.FailOverlay;
|
||||
|
||||
public FakeImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
|
||||
: base(allowPause, showResults, pauseOnFocusLost)
|
||||
{
|
||||
|
@ -440,8 +440,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddStep("import old classic skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"classic-layout-version-0.osk").SkinInfo);
|
||||
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
|
||||
AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<LegacyComboCounter>().Any());
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
|
||||
AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Any());
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
|
||||
AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyDefaultComboCounter
|
||||
{
|
||||
@ -454,8 +454,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault());
|
||||
AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin);
|
||||
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
|
||||
AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
|
||||
AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
}
|
||||
|
||||
private Skin importSkinFromArchives(string filename)
|
||||
|
@ -19,6 +19,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
@ -167,14 +168,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestSpectatingDuringGameplay()
|
||||
{
|
||||
start();
|
||||
sendFrames(300);
|
||||
sendFrames(300, initialResultCount: 100);
|
||||
|
||||
loadSpectatingScreen();
|
||||
waitForPlayerCurrent();
|
||||
|
||||
sendFrames(300);
|
||||
sendFrames(300, initialResultCount: 100);
|
||||
|
||||
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000));
|
||||
AddAssert("check judgement counts are correct", () => player.ChildrenOfType<JudgementCountController>().Single().Counters.Sum(c => c.ResultCount.Value),
|
||||
() => Is.GreaterThanOrEqualTo(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -405,9 +408,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private void checkPaused(bool state) =>
|
||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
||||
|
||||
private void sendFrames(int count = 10, double startTime = 0)
|
||||
private void sendFrames(int count = 10, double startTime = 0, int initialResultCount = 0)
|
||||
{
|
||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime));
|
||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime, initialResultCount));
|
||||
}
|
||||
|
||||
private void loadSpectatingScreen()
|
||||
|
@ -144,6 +144,28 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
exitViaEscapeAndConfirm();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnterGameplayWhileFilteringToNoSelection()
|
||||
{
|
||||
TestPlaySongSelect songSelect = null;
|
||||
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("force selection", () =>
|
||||
{
|
||||
songSelect.FinaliseSelection();
|
||||
songSelect.FilterControl.CurrentTextSearch.Value = "test";
|
||||
});
|
||||
|
||||
AddUntilStep("wait for player", () => !songSelect.IsCurrentScreen());
|
||||
AddStep("return to song select", () => songSelect.MakeCurrent());
|
||||
|
||||
AddUntilStep("wait for selection lost", () => songSelect.Beatmap.IsDefault);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSongSelectBackActionHandling()
|
||||
{
|
||||
|
@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Test]
|
||||
public void TestPlayCountRankingTier()
|
||||
{
|
||||
AddAssert("1 before silver", () => DailyChallengeStatsDisplay.TierForPlayCount(30) == RankingTier.Bronze);
|
||||
AddAssert("first silver", () => DailyChallengeStatsDisplay.TierForPlayCount(31) == RankingTier.Silver);
|
||||
AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze);
|
||||
AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,18 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
@ -23,6 +27,7 @@ using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Ranking.Statistics.User;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
@ -80,6 +85,69 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
loadPanel(null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStatisticsShownCorrectlyIfUpdateDeliveredBeforeLoad()
|
||||
{
|
||||
UserStatisticsWatcher userStatisticsWatcher = null!;
|
||||
ScoreInfo score = null!;
|
||||
|
||||
AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher()));
|
||||
AddStep("set user statistics update", () =>
|
||||
{
|
||||
score = TestResources.CreateTestScoreInfo();
|
||||
score.OnlineID = 1234;
|
||||
((Bindable<UserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score,
|
||||
new UserStatistics
|
||||
{
|
||||
Level = new UserStatistics.LevelInfo
|
||||
{
|
||||
Current = 5,
|
||||
Progress = 20,
|
||||
},
|
||||
GlobalRank = 38000,
|
||||
CountryRank = 12006,
|
||||
PP = 2134,
|
||||
RankedScore = 21123849,
|
||||
Accuracy = 0.985,
|
||||
PlayCount = 13375,
|
||||
PlayTime = 354490,
|
||||
TotalScore = 128749597,
|
||||
TotalHits = 0,
|
||||
MaxCombo = 1233,
|
||||
}, new UserStatistics
|
||||
{
|
||||
Level = new UserStatistics.LevelInfo
|
||||
{
|
||||
Current = 5,
|
||||
Progress = 30,
|
||||
},
|
||||
GlobalRank = 36000,
|
||||
CountryRank = 12000,
|
||||
PP = (decimal)2134.5,
|
||||
RankedScore = 23897015,
|
||||
Accuracy = 0.984,
|
||||
PlayCount = 13376,
|
||||
PlayTime = 35789,
|
||||
TotalScore = 132218497,
|
||||
TotalHits = 0,
|
||||
MaxCombo = 1233,
|
||||
});
|
||||
});
|
||||
AddStep("load user statistics panel", () => Child = new DependencyProvidingContainer
|
||||
{
|
||||
CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)],
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new UserStatisticsPanel(score)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible },
|
||||
Score = { Value = score, }
|
||||
}
|
||||
});
|
||||
AddUntilStep("overall ranking present", () => this.ChildrenOfType<OverallRanking>().Any());
|
||||
AddUntilStep("loading spinner not visible", () => this.ChildrenOfType<LoadingLayer>().All(l => l.State.Value == Visibility.Hidden));
|
||||
}
|
||||
|
||||
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
|
||||
{
|
||||
Child = new UserStatisticsPanel(score)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user