1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 02:29:53 +08:00

Compare commits

...

957 Commits

561 changed files with 18238 additions and 4215 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
]
},
"nvika": {
"version": "3.0.0",
"version": "4.0.0",
"commands": [
"nvika"
]
+1 -4
View File
@@ -114,10 +114,7 @@ jobs:
dotnet-version: "8.0.x"
- name: Install .NET workloads
# since windows image 20241113.3.0, not specifying a version here
# installs the .NET 7 version of android workload for very unknown reasons.
# revisit once we upgrade to .NET 9, it's probably fixed there.
run: dotnet workload install android --version (dotnet --version)
run: dotnet workload install android
- name: Compile
run: dotnet build -c Debug osu.Android.slnf
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="osu.Android">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
+4 -1
View File
@@ -51,8 +51,11 @@ dotnet_diagnostic.IDE1006.severity = warning
# Too many noisy warnings for parsing/formatting numbers
dotnet_diagnostic.CA1305.severity = none
# messagepack complains about "osu" not being title cased due to reserved words
dotnet_diagnostic.CS8981.severity = none
# CA1507: Use nameof to express symbol names
# Flaggs serialization name attributes
# Flags serialization name attributes
dotnet_diagnostic.CA1507.severity = suggestion
# CA1806: Do not ignore method results
+1 -1
View File
@@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu!
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024.
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
## Developing a custom ruleset
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
@@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
}
}
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
@@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
}
}
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
@@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1206.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.220.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -1,34 +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 Android.Content.PM;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play;
namespace osu.Android
{
public partial class GameplayScreenRotationLocker : Component
{
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
[Resolved]
private OsuGameActivity gameActivity { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserPlayInfo)
{
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
localUserPlaying.BindValueChanged(updateLock, true);
}
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
{
gameActivity.RunOnUiThread(() =>
{
gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
});
}
}
}
+40 -12
View File
@@ -13,7 +13,6 @@ using Android.Graphics;
using Android.OS;
using Android.Views;
using osu.Framework.Android;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Database;
using Debug = System.Diagnostics.Debug;
using Uri = Android.Net.Uri;
@@ -50,9 +49,25 @@ namespace osu.Android
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
private OsuGameAndroid game = null!;
public new bool IsTablet { get; private set; }
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
private readonly OsuGameAndroid game;
private bool gameCreated;
protected override Framework.Game CreateGame()
{
if (gameCreated)
throw new InvalidOperationException("Framework tried to create a game twice.");
gameCreated = true;
return game;
}
public OsuGameActivity()
{
game = new OsuGameAndroid(this);
}
protected override void OnCreate(Bundle? savedInstanceState)
{
@@ -76,9 +91,9 @@ namespace osu.Android
WindowManager.DefaultDisplay.GetSize(displaySize);
#pragma warning restore CA1422
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f;
IsTablet = smallestWidthDp >= 600f;
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
// Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
// The assembly files are not available as files either after native AOT.
@@ -95,25 +110,38 @@ namespace osu.Android
private void handleIntent(Intent? intent)
{
switch (intent?.Action)
if (intent == null)
return;
switch (intent.Action)
{
case Intent.ActionDefault:
if (intent.Scheme == ContentResolver.SchemeContent)
handleImportFromUris(intent.Data.AsNonNull());
{
if (intent.Data != null)
handleImportFromUris(intent.Data);
}
else if (osu_url_schemes.Contains(intent.Scheme))
game.HandleLink(intent.DataString);
{
if (intent.DataString != null)
game.HandleLink(intent.DataString);
}
break;
case Intent.ActionSend:
case Intent.ActionSendMultiple:
{
if (intent.ClipData == null)
break;
var uris = new List<Uri>();
for (int i = 0; i < intent.ClipData?.ItemCount; i++)
for (int i = 0; i < intent.ClipData.ItemCount; i++)
{
var content = intent.ClipData?.GetItemAt(i);
if (content != null)
uris.Add(content.Uri.AsNonNull());
var item = intent.ClipData.GetItemAt(i);
if (item?.Uri != null)
uris.Add(item.Uri);
}
handleImportFromUris(uris.ToArray());
+34 -1
View File
@@ -3,13 +3,16 @@
using System;
using Android.App;
using Android.Content.PM;
using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Screens;
using osu.Game.Updater;
using osu.Game.Utils;
using osuTK;
namespace osu.Android
{
@@ -18,6 +21,8 @@ namespace osu.Android
[Cached]
private readonly OsuGameActivity gameActivity;
protected override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{
@@ -71,7 +76,35 @@ namespace osu.Android
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
UserPlayingState.BindValueChanged(_ => updateOrientation());
}
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
{
base.ScreenChanged(current, newScreen);
if (newScreen != null)
updateOrientation();
}
private void updateOrientation()
{
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet);
switch (orientation)
{
case MobileUtils.Orientation.Locked:
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
break;
case MobileUtils.Orientation.Portrait:
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
break;
case MobileUtils.Orientation.Default:
gameActivity.RequestedOrientation = gameActivity.DefaultOrientation;
break;
}
}
public override void SetHost(GameHost host)
+18 -27
View File
@@ -51,12 +51,9 @@ namespace osu.Desktop
[Resolved]
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
private IBindable<DiscordRichPresenceMode> privacyMode = null!;
private IBindable<UserStatus> userStatus = null!;
private IBindable<UserActivity?> userActivity = null!;
private readonly RichPresence presence = new RichPresence
{
@@ -71,8 +68,12 @@ namespace osu.Desktop
private IBindable<APIUser>? user;
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config, SessionStatics session)
{
privacyMode = config.GetBindable<DiscordRichPresenceMode>(OsuSetting.DiscordRichPresence);
userStatus = config.GetBindable<UserStatus>(OsuSetting.UserOnlineStatus);
userActivity = session.GetBindable<UserActivity?>(Static.UserOnlineActivity);
client = new DiscordRpcClient(client_id)
{
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
@@ -81,7 +82,7 @@ namespace osu.Desktop
};
client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network);
try
{
@@ -105,21 +106,11 @@ namespace osu.Desktop
{
base.LoadComplete();
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
user = api.LocalUser.GetBoundCopy();
user.BindValueChanged(u =>
{
status.UnbindBindings();
status.BindTo(u.NewValue.Status);
activity.UnbindBindings();
activity.BindTo(u.NewValue.Activity);
}, true);
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => schedulePresenceUpdate());
userStatus.BindValueChanged(_ => schedulePresenceUpdate());
userActivity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated;
@@ -151,13 +142,13 @@ namespace osu.Desktop
if (!client.IsInitialized)
return;
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
if (!api.IsLoggedIn || userStatus.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
{
client.ClearPresence();
return;
}
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || userStatus.Value == UserStatus.DoNotDisturb;
updatePresence(hideIdentifiableInformation);
client.SetPresence(presence);
@@ -170,19 +161,19 @@ namespace osu.Desktop
return;
// user activity
if (activity.Value != null)
if (userActivity.Value != null)
{
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
presence.State = clampLength(userActivity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(userActivity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
if (userActivity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
{
presence.Buttons = new[]
{
new Button
{
Label = "View beatmap",
Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
}
};
}
+8 -4
View File
@@ -67,7 +67,12 @@ namespace osu.Desktop
{
try
{
stableInstallPath = getStableInstallPathFromRegistry();
stableInstallPath = getStableInstallPathFromRegistry("osustable.File.osz");
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath;
stableInstallPath = getStableInstallPathFromRegistry("osu!");
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath;
@@ -89,9 +94,9 @@ namespace osu.Desktop
}
[SupportedOSPlatform("windows")]
private string? getStableInstallPathFromRegistry()
private string? getStableInstallPathFromRegistry(string progId)
{
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey(progId))
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
}
@@ -134,7 +139,6 @@ namespace osu.Desktop
if (iconStream != null)
host.Window.SetIconFromStream(iconStream);
host.Window.CursorState |= CursorState.Hidden;
host.Window.Title = Name;
}
@@ -30,8 +30,6 @@ namespace osu.Desktop.Security
private partial class ElevatedPrivilegesNotification : SimpleNotification
{
public override bool IsImportant => true;
public ElevatedPrivilegesNotification()
{
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
+149 -46
View File
@@ -17,6 +17,7 @@ namespace osu.Desktop.Windows
public static class WindowsAssociationManager
{
private const string software_classes = @"Software\Classes";
private const string software_registered_applications = @"Software\RegisteredApplications";
/// <summary>
/// Sub key for setting the icon.
@@ -36,7 +37,11 @@ namespace osu.Desktop.Windows
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
/// </summary>
private const string program_id_prefix = "osu.File";
private const string program_id_file_prefix = "osu.File";
private const string program_id_protocol_prefix = "osu.Uri";
private static readonly ApplicationCapability application_capability = new ApplicationCapability(@"osu", @"Software\ppy\osu\Capabilities", "osu!(lazer)");
private static readonly FileAssociation[] file_associations =
{
@@ -56,14 +61,13 @@ namespace osu.Desktop.Windows
/// Installs file and URI associations.
/// </summary>
/// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks>
public static void InstallAssociations()
{
try
{
updateAssociations();
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
NotifyShellUpdate();
}
catch (Exception e)
@@ -76,17 +80,13 @@ namespace osu.Desktop.Windows
/// Updates associations with latest definitions.
/// </summary>
/// <remarks>
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
/// </remarks>
public static void UpdateAssociations()
{
try
{
updateAssociations();
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
NotifyShellUpdate();
}
catch (Exception e)
@@ -95,11 +95,19 @@ namespace osu.Desktop.Windows
}
}
public static void UpdateDescriptions(LocalisationManager localisationManager)
// TODO: call this sometime.
public static void LocaliseDescriptions(LocalisationManager localisationManager)
{
try
{
updateDescriptions(localisationManager);
application_capability.LocaliseDescription(localisationManager);
foreach (var association in file_associations)
association.LocaliseDescription(localisationManager);
foreach (var association in uri_associations)
association.LocaliseDescription(localisationManager);
NotifyShellUpdate();
}
catch (Exception e)
@@ -112,6 +120,8 @@ namespace osu.Desktop.Windows
{
try
{
application_capability.Uninstall();
foreach (var association in file_associations)
association.Uninstall();
@@ -133,22 +143,16 @@ namespace osu.Desktop.Windows
/// </summary>
private static void updateAssociations()
{
application_capability.Install();
foreach (var association in file_associations)
association.Install();
foreach (var association in uri_associations)
association.Install();
}
private static void updateDescriptions(LocalisationManager? localisation)
{
foreach (var association in file_associations)
association.UpdateDescription(getLocalisedString(association.Description));
foreach (var association in uri_associations)
association.UpdateDescription(getLocalisedString(association.Description));
string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString();
application_capability.RegisterFileAssociations(file_associations);
application_capability.RegisterUriAssociations(uri_associations);
}
#region Native interop
@@ -174,9 +178,87 @@ namespace osu.Desktop.Windows
#endregion
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
private class ApplicationCapability
{
private string programId => $@"{program_id_prefix}{Extension}";
private string uniqueName { get; }
private string capabilityPath { get; }
private LocalisableString description { get; }
public ApplicationCapability(string uniqueName, string capabilityPath, LocalisableString description)
{
this.uniqueName = uniqueName;
this.capabilityPath = capabilityPath;
this.description = description;
}
/// <summary>
/// Registers an application capability according to <see href="https://learn.microsoft.com/en-us/windows/win32/shell/default-programs#registering-an-application-for-use-with-default-programs">
/// Registering an Application for Use with Default Programs</see>.
/// </summary>
public void Install()
{
using (var capability = Registry.CurrentUser.CreateSubKey(capabilityPath))
{
capability.SetValue(@"ApplicationDescription", description.ToString());
}
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
registeredApplications?.SetValue(uniqueName, capabilityPath);
}
public void RegisterFileAssociations(FileAssociation[] associations)
{
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
if (capability == null) return;
using var fileAssociations = capability.CreateSubKey(@"FileAssociations");
foreach (var association in associations)
fileAssociations.SetValue(association.Extension, association.ProgramId);
}
public void RegisterUriAssociations(UriAssociation[] associations)
{
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
if (capability == null) return;
using var urlAssociations = capability.CreateSubKey(@"UrlAssociations");
foreach (var association in associations)
urlAssociations.SetValue(association.Protocol, association.ProgramId);
}
public void LocaliseDescription(LocalisationManager localisationManager)
{
using (var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true))
{
capability?.SetValue(@"ApplicationDescription", localisationManager.GetLocalisedString(description));
}
}
public void Uninstall()
{
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
registeredApplications?.DeleteValue(uniqueName, throwOnMissingValue: false);
Registry.CurrentUser.DeleteSubKeyTree(capabilityPath, throwOnMissingSubKey: false);
}
}
private class FileAssociation
{
public string ProgramId => $@"{program_id_file_prefix}{Extension}";
public string Extension { get; }
private LocalisableString description { get; }
private string iconPath { get; }
public FileAssociation(string extension, LocalisableString description, string iconPath)
{
Extension = extension;
this.description = description;
this.iconPath = iconPath;
}
/// <summary>
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
@@ -187,10 +269,12 @@ namespace osu.Desktop.Windows
if (classes == null) return;
// register a program id for the given extension
using (var programKey = classes.CreateSubKey(programId))
using (var programKey = classes.CreateSubKey(ProgramId))
{
programKey.SetValue(null, description.ToString());
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath);
defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
@@ -198,23 +282,25 @@ namespace osu.Desktop.Windows
using (var extensionKey = classes.CreateSubKey(Extension))
{
// set ourselves as the default program
extensionKey.SetValue(null, programId);
// Clear out our existing default ProgramID. Default programs in Windows are handled internally by Explorer,
// so having it here is just confusing and may override user preferences.
if (extensionKey.GetValue(null) is string s && s == ProgramId)
extensionKey.SetValue(null, string.Empty);
// add to the open with dialog
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
openWithKey.SetValue(programId, string.Empty);
openWithKey.SetValue(ProgramId, string.Empty);
}
}
public void UpdateDescription(string description)
public void LocaliseDescription(LocalisationManager localisationManager)
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var programKey = classes.OpenSubKey(programId, true))
programKey?.SetValue(null, description);
using (var programKey = classes.OpenSubKey(ProgramId, true))
programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
}
/// <summary>
@@ -227,26 +313,34 @@ namespace osu.Desktop.Windows
using (var extensionKey = classes.OpenSubKey(Extension, true))
{
// clear our default association so that Explorer doesn't show the raw programId to users
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
if (extensionKey?.GetValue(null) is string s && s == programId)
extensionKey.SetValue(null, string.Empty);
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
openWithKey?.DeleteValue(ProgramId, throwOnMissingValue: false);
}
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
classes.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
}
}
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
private class UriAssociation
{
/// <summary>
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
/// </summary>
public const string URL_PROTOCOL = @"URL Protocol";
private const string url_protocol = @"URL Protocol";
public string Protocol { get; }
private LocalisableString description { get; }
private string iconPath { get; }
public UriAssociation(string protocol, LocalisableString description, string iconPath)
{
Protocol = protocol;
this.description = description;
this.iconPath = iconPath;
}
public string ProgramId => $@"{program_id_protocol_prefix}.{Protocol}";
/// <summary>
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
@@ -258,29 +352,38 @@ namespace osu.Desktop.Windows
using (var protocolKey = classes.CreateSubKey(Protocol))
{
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
protocolKey.SetValue(null, $@"URL:{description}");
protocolKey.SetValue(url_protocol, string.Empty);
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, IconPath);
// clear out old data
protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false);
protocolKey.DeleteSubKeyTree(@"Shell", throwOnMissingSubKey: false);
}
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
// register a program id for the given protocol
using (var programKey = classes.CreateSubKey(ProgramId))
{
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
defaultIconKey.SetValue(null, iconPath);
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
}
}
public void UpdateDescription(string description)
public void LocaliseDescription(LocalisationManager localisationManager)
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
if (classes == null) return;
using (var protocolKey = classes.OpenSubKey(Protocol, true))
protocolKey?.SetValue(null, $@"URL:{description}");
protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
}
public void Uninstall()
{
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
}
}
}
+2 -2
View File
@@ -24,9 +24,9 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
<PackageReference Include="System.IO.Packaging" Version="9.0.2" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.915" />
<PackageReference Include="Velopack" Version="0.0.1053" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
@@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,15 @@
// 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 Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}
@@ -1,16 +1,15 @@
// 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.iOS;
using osu.Game.Tests;
using UIKit;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuTestBrowser());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
@@ -12,7 +12,6 @@ using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
@@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
}
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
protected override void UpdatePlacementTimeAndPosition()
{
var result = base.SnapForBlueprint(blueprint);
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
return result;
var position = InputManager.CurrentState.Mouse.Position;
double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP;
CurrentBlueprint.UpdateTimeAndPosition(position, time);
}
}
}
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{
private JuiceStream hitObject;
private JuiceStream hitObject = null!;
private readonly ManualClock manualClock = new ManualClock();
@@ -193,6 +191,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addVertexCheckStep(1, 0, times[0], positions[0]);
}
[Test]
public void TestDeletingSecondVertexDeletesEntireJuiceStream()
{
double[] times = { 100, 400 };
float[] positions = { 100, 150 };
addBlueprintStep(times, positions);
addDeleteVertexSteps(times[1], positions[1]);
AddAssert("juice stream deleted", () => EditorBeatmap.HitObjects, () => Is.Empty);
}
[Test]
public void TestVertexResampling()
{
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
+2 -1
View File
@@ -14,6 +14,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Edit.Setup;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
[
new MetadataSection(),
new DifficultySection(),
new CatchDifficultySection(),
new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
@@ -7,6 +7,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
@@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
base.UpdateTimeAndPosition(result);
var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
if (!(result.Time is double time)) return;
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
if (!(result.Time is double time)) return result;
switch (PlacementActive)
{
@@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
return result;
}
}
}
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
public abstract partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
where THitObject : CatchHitObject, new()
{
protected new THitObject HitObject => (THitObject)base.HitObject;
@@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
[Resolved]
private Playfield playfield { get; set; } = null!;
public CatchPlacementBlueprint()
[Resolved]
protected CatchHitObjectComposer? Composer { get; private set; }
protected CatchPlacementBlueprint()
: base(new THitObject())
{
}
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
}));
}
public void UpdateHitObjectFromPath(JuiceStream hitObject)
public virtual void UpdateHitObjectFromPath(JuiceStream hitObject)
{
// The SV setting may need to be changed for the current path.
var svBindable = hitObject.SliderVelocityMultiplierBindable;
@@ -138,5 +138,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
EditorBeatmap?.EndChange();
}
public override void UpdateHitObjectFromPath(JuiceStream hitObject)
{
base.UpdateHitObjectFromPath(hitObject);
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
EditorBeatmap?.Remove(hitObject);
}
}
}
@@ -5,6 +5,7 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
@@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return true;
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
base.UpdateTimeAndPosition(result);
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
return result;
}
}
}
@@ -83,15 +83,22 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
switch (PlacementActive)
{
case PlacementState.Waiting:
if (!(result.Time is double snappedTime)) return;
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
HitObject.StartTime = snappedTime;
if (result.Time is double snappedTime)
HitObject.StartTime = snappedTime;
break;
case PlacementState.Active:
@@ -100,28 +107,21 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
break;
default:
return;
return result;
}
// Make sure the up-to-date position is used for outlines.
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
updateHitObjectFromPath();
}
if (lastEditablePathId != editablePath.PathId)
editablePath.UpdateHitObjectFromPath(HitObject);
lastEditablePathId = editablePath.PathId;
private void updateHitObjectFromPath()
{
if (lastEditablePathId == editablePath.PathId)
return;
editablePath.UpdateHitObjectFromPath(HitObject);
ApplyDefaultsToHitObject();
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
lastEditablePathId = editablePath.PathId;
return result;
}
private double positionToTime(float relativeYPosition)
@@ -1,16 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
public partial class CatchBlueprintContainer : ComposeBlueprintContainer
{
public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer;
public CatchBlueprintContainer(CatchHitObjectComposer composer)
: base(composer)
{
@@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit
}
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition);
gridSnapResult.ScreenSpacePosition.X = movePosition.X;
var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
}
}
@@ -17,7 +17,7 @@ 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 expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
@@ -18,15 +18,15 @@ using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
[Cached]
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
{
private const float distance_snap_radius = 50;
public const float DISTANCE_SNAP_RADIUS = 50;
private CatchDistanceSnapGrid distanceSnapGrid = null!;
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
protected override IEnumerable<Drawable> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons());
@@ -136,22 +136,12 @@ namespace osu.Game.Rulesets.Catch.Edit
DistanceSnapProvider.HandleToggleViaKey(key);
}
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition)
{
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult)
return snapResult;
result.ScreenSpacePosition.X = screenSpacePosition.X;
if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
{
result = snapResult;
}
}
return result;
return null;
}
private PalpableCatchHitObject? getLastSnappableHitObject(double time)
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
using Direction = osu.Framework.Graphics.Direction;
namespace osu.Game.Rulesets.Catch.Edit
@@ -38,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Edit
return true;
}
moveSelection(deltaX);
return true;
}
private void moveSelection(float deltaX)
{
EditorBeatmap.PerformOnSelection(h =>
{
if (!(h is CatchHitObject catchObject)) return;
@@ -48,7 +57,60 @@ namespace osu.Game.Rulesets.Catch.Edit
foreach (var nested in catchObject.NestedHitObjects.OfType<CatchHitObject>())
nested.OriginalX += deltaX;
});
}
private bool nudgeMovementActive;
protected override bool OnKeyDown(KeyDownEvent e)
{
// Until the keys below are global actions, this will prevent conflicts with "seek between sample points"
// which has a default of ctrl+shift+arrows.
if (e.ShiftPressed)
return false;
if (e.ControlPressed)
{
switch (e.Key)
{
case Key.Left:
return nudgeSelection(-1);
case Key.Right:
return nudgeSelection(1);
}
}
return false;
}
protected override void OnKeyUp(KeyUpEvent e)
{
base.OnKeyUp(e);
if (nudgeMovementActive && !e.ControlPressed)
{
EditorBeatmap.EndChange();
nudgeMovementActive = false;
}
}
/// <summary>
/// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints).
/// </summary>
private bool nudgeSelection(float deltaX)
{
if (!nudgeMovementActive)
{
nudgeMovementActive = true;
EditorBeatmap.BeginChange();
}
var firstBlueprint = SelectedBlueprints.FirstOrDefault();
if (firstBlueprint == null)
return false;
moveSelection(deltaX);
return true;
}
@@ -0,0 +1,125 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Catch.Edit.Setup
{
public partial class CatchDifficultySection : SetupSection
{
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
circleSizeSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsCs,
HintText = EditorSetupStrings.CircleSizeDescription,
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
healthDrainSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsDrain,
HintText = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
approachRateSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsAr,
HintText = EditorSetupStrings.ApproachRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
baseVelocitySlider = new FormSliderBar<double>
{
Caption = EditorSetupStrings.BaseVelocity,
HintText = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
MinValue = 0.4,
MaxValue = 3.6,
Precision = 0.01f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
tickRateSlider = new FormSliderBar<double>
{
Caption = EditorSetupStrings.TickRate,
HintText = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
MinValue = 1,
MaxValue = 4,
Precision = 1,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
};
foreach (var item in Children.OfType<FormSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<FormSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues();
}
private void updateValues()
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}
@@ -159,27 +159,26 @@ namespace osu.Game.Rulesets.Catch.Objects
{
// Note that this implementation is shared with the osu! ruleset's implementation.
// If a change is made here, OsuHitObject.cs should also be updated.
ComboIndex = lastObj?.ComboIndex ?? 0;
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
int index = lastObj?.ComboIndex ?? 0;
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
if (this is BananaShower)
// - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
// - At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
if (this is not BananaShower && (NewCombo || lastObj == null || lastObj is BananaShower))
{
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
return;
}
// At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
if (NewCombo || lastObj == null || lastObj is BananaShower)
{
IndexInCurrentCombo = 0;
ComboIndex++;
ComboIndexWithOffsets += ComboOffset + 1;
inCurrentCombo = 0;
index++;
indexWithOffsets += ComboOffset + 1;
if (lastObj != null)
lastObj.LastInCombo = true;
}
ComboIndex = index;
ComboIndexWithOffsets = indexWithOffsets;
IndexInCurrentCombo = inCurrentCombo;
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
@@ -210,11 +209,27 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
float IHasXPosition.X => OriginalX;
float IHasXPosition.X
{
get => OriginalX;
set => OriginalX = value;
}
float IHasYPosition.Y => LegacyConvertedY;
float IHasYPosition.Y
{
get => LegacyConvertedY;
set => LegacyConvertedY = value;
}
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
Vector2 IHasPosition.Position
{
get => new Vector2(OriginalX, LegacyConvertedY);
set
{
((IHasXPosition)this).X = value.X;
((IHasYPosition)this).Y = value.Y;
}
}
#endregion
}
@@ -4,6 +4,7 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (keyCounter != null)
{
@@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f;
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
})
{
Children = new Drawable[]
{
new LegacyKeyCounterDisplay(),
new SpectatorList(),
}
};
}
@@ -0,0 +1,15 @@
// 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 Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}
@@ -1,16 +1,15 @@
// 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.iOS;
using osu.Game.Tests;
using UIKit;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuTestBrowser());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
@@ -9,7 +9,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
@@ -47,12 +46,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
});
}
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
protected override void UpdatePlacementTimeAndPosition()
{
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
var pos = column.ScreenSpacePositionAtTime(time);
return new SnapResult(pos, time, column);
CurrentBlueprint.UpdateTimeAndPosition(pos, time);
}
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
@@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
@@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
set => InternalChild = value;
}
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
throw new NotImplementedException();
}
}
}
@@ -22,9 +22,11 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase("basic")]
[TestCase("zero-length-slider")]
[TestCase("mania-specific-spinner")]
[TestCase("20544")]
[TestCase("100374")]
[TestCase("1450162")]
[TestCase("4869637")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{
CreateModTest(new ModTestData
{
Mod = new ManiaModHidden(),
Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
});
}
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{
CreateModTest(new ModTestData
{
Mod = new ManiaModHidden(),
Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
});
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{
CreateModTest(new ModTestData
{
Mod = new ManiaModHidden(),
Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
});
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{
CreateModTest(new ModTestData
{
Mod = new ManiaModHidden(),
Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
});
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{
CreateModTest(new ModTestData
{
Mod = new ManiaModHidden(),
Mod = new ManiaModFadeIn(),
CreateBeatmap = () => new Beatmap
{
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,60 @@
{
"Mappings": [
{
"RandomW": 273071671,
"RandomX": 842502087,
"RandomY": 3579807591,
"RandomZ": 273326509,
"StartTime": 11783.0,
"Objects": [
{
"StartTime": 11783.0,
"EndTime": 15116.0,
"Column": 0
}
]
},
{
"RandomW": 2659271247,
"RandomX": 3579807591,
"RandomY": 273326509,
"RandomZ": 273071671,
"StartTime": 91545.0,
"Objects": [
{
"StartTime": 91545.0,
"EndTime": 92735.0,
"Column": 0
}
]
},
{
"RandomW": 3083635271,
"RandomX": 273326509,
"RandomY": 273071671,
"RandomZ": 2659271247,
"StartTime": 152497.0,
"Objects": [
{
"StartTime": 152497.0,
"EndTime": 153687.0,
"Column": 1
}
]
},
{
"RandomW": 4073591514,
"RandomX": 273071671,
"RandomY": 2659271247,
"RandomZ": 3083635271,
"StartTime": 231545.0,
"Objects": [
{
"StartTime": 231545.0,
"EndTime": 232974.0,
"Column": 3
}
]
}
]
}
@@ -0,0 +1,27 @@
osu file format v14
[General]
Mode: 3
[Difficulty]
HPDrainRate:5
CircleSize:4
OverallDifficulty:5
ApproachRate:0
SliderMultiplier:2.6
SliderTickRate:1
[TimingPoints]
355,476.190476190476,4,2,1,60,1,0
60652,-100,4,2,1,60,0,1
92735,-100,4,2,1,60,0,0
121485,-100,4,2,1,60,0,1
153688,-100,4,2,1,60,0,0
182497,-100,4,2,1,60,0,1
213688,-100,4,2,1,60,0,0
[HitObjects]
256,192,11783,12,0,15116,0:0:0:0:
256,192,91545,12,0,92735,0:0:0:0:
256,192,152497,12,0,153687,0:0:0:0:
256,192,231545,12,0,232974,0:0:0:0:
@@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new ColumnHitObjectArea(new HitObjectContainer())
Child = new ColumnHitObjectArea
{
RelativeSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both,
Child = new HitObjectContainer(),
}
},
new ColumnTestContainer(1, ManiaAction.Key2)
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new ColumnHitObjectArea(new HitObjectContainer())
Child = new ColumnHitObjectArea
{
RelativeSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both,
Child = new HitObjectContainer(),
}
}
}
@@ -0,0 +1,68 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaTouchInput : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestTouchInput()
{
for (int i = 0; i < 4; i++)
{
int index = i;
AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(index).Action.Value));
AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action released",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Not.Contain(getColumn(index).Action.Value));
}
}
[Test]
public void TestOneColumnMultipleTouches()
{
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action still pressed",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action still pressed",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action released",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Not.Contain(getColumn(0).Action.Value));
}
private Column getColumn(int index) => this.ChildrenOfType<Column>().ElementAt(index);
}
}
@@ -1,49 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestTouchAreaNotInitiallyVisible()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
}
[Test]
public void TestPressReceptors()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
for (int i = 0; i < 4; i++)
{
int index = i;
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getReceptor(index).Action.Value));
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
}
}
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
}
}
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
@@ -7,11 +7,13 @@ using System.Linq;
using System.Collections.Generic;
using System.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Utils;
using osuTK;
@@ -124,16 +126,109 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
{
if (original is ManiaHitObject maniaOriginal)
{
yield return maniaOriginal;
LegacyHitObjectType legacyType;
yield break;
switch (original)
{
case ManiaHitObject maniaObj:
{
yield return maniaObj;
yield break;
}
case IHasLegacyHitObjectType legacy:
legacyType = legacy.LegacyType & LegacyHitObjectType.ObjectTypes;
break;
case IHasPath:
legacyType = LegacyHitObjectType.Slider;
break;
case IHasDuration:
legacyType = LegacyHitObjectType.Hold;
break;
default:
legacyType = LegacyHitObjectType.Circle;
break;
}
var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap);
foreach (ManiaHitObject obj in objects)
yield return obj;
double startTime = original.StartTime;
double endTime = (original as IHasDuration)?.EndTime ?? startTime;
Vector2 position = (original as IHasPosition)?.Position ?? Vector2.Zero;
PatternGenerator conversion;
switch (legacyType)
{
case LegacyHitObjectType.Circle:
if (IsForCurrentRuleset)
{
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
recordNote(startTime, position);
}
else
{
// Note: The density is used during the pattern generator constructor, and intentionally computed first.
computeDensity(startTime);
conversion = new HitCirclePatternGenerator(Random, original, beatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
recordNote(startTime, position);
}
break;
case LegacyHitObjectType.Slider:
if (IsForCurrentRuleset)
{
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
recordNote(original.StartTime, position);
}
else
{
var generator = new SliderPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
conversion = generator;
for (int i = 0; i <= generator.SpanCount; i++)
{
double time = original.StartTime + generator.SegmentDuration * i;
recordNote(time, position);
computeDensity(time);
}
}
break;
case LegacyHitObjectType.Spinner:
// Note: Some older mania-specific beatmaps can have spinners that are converted rather than passed through.
// Newer beatmaps will usually use the "hold" hitobject type below.
conversion = new SpinnerPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
recordNote(endTime, new Vector2(256, 192));
computeDensity(endTime);
break;
case LegacyHitObjectType.Hold:
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
recordNote(endTime, position);
computeDensity(endTime);
break;
default:
throw new ArgumentException($"Invalid legacy object type: {legacyType}", nameof(original));
}
foreach (var newPattern in conversion.Generate())
{
if (conversion is HitCirclePatternGenerator circleGenerator)
lastStair = circleGenerator.StairType;
if (conversion is HitCirclePatternGenerator || conversion is SliderPatternGenerator)
lastPattern = newPattern;
foreach (var obj in newPattern.HitObjects)
yield return obj;
}
}
private readonly LimitedCapacityQueue<double> prevNoteTimes = new LimitedCapacityQueue<double>(max_notes_for_density);
@@ -156,135 +251,5 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
lastTime = time;
lastPosition = position;
}
/// <summary>
/// Method that generates hit objects for osu!mania specific beatmaps.
/// </summary>
/// <param name="original">The original hit object.</param>
/// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, IBeatmap originalBeatmap)
{
var generator = new SpecificBeatmapPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
foreach (var newPattern in generator.Generate())
{
lastPattern = newPattern;
foreach (var obj in newPattern.HitObjects)
yield return obj;
}
}
/// <summary>
/// Method that generates hit objects for non-osu!mania beatmaps.
/// </summary>
/// <param name="original">The original hit object.</param>
/// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
/// <returns>The hit objects generated.</returns>
private IEnumerable<ManiaHitObject> generateConverted(HitObject original, IBeatmap originalBeatmap)
{
Patterns.PatternGenerator? conversion = null;
switch (original)
{
case IHasPath:
{
var generator = new PathObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
conversion = generator;
var positionData = original as IHasPosition;
for (int i = 0; i <= generator.SpanCount; i++)
{
double time = original.StartTime + generator.SegmentDuration * i;
recordNote(time, positionData?.Position ?? Vector2.Zero);
computeDensity(time);
}
break;
}
case IHasDuration endTimeData:
{
conversion = new EndTimeObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);
break;
}
case IHasPosition positionData:
{
computeDensity(original.StartTime);
conversion = new HitObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
recordNote(original.StartTime, positionData.Position);
break;
}
}
if (conversion == null)
yield break;
foreach (var newPattern in conversion.Generate())
{
lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern;
lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair;
foreach (var obj in newPattern.HitObjects)
yield return obj;
}
}
/// <summary>
/// A pattern generator for osu!mania-specific beatmaps.
/// </summary>
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
{
public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
}
public override IEnumerable<Pattern> Generate()
{
yield return generate();
}
private Pattern generate()
{
var positionData = HitObject as IHasXPosition;
int column = GetColumn(positionData?.X ?? 0);
var pattern = new Pattern();
if (HitObject is IHasDuration endTimeData)
{
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Duration = endTimeData.Duration,
Column = column,
Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
});
}
else if (HitObject is IHasXPosition)
{
pattern.Add(new Note
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Column = column
});
}
return pattern;
}
}
}
}
@@ -16,13 +16,16 @@ using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class HitObjectPatternGenerator : PatternGenerator
/// <summary>
/// Converter for legacy "HitCircle" hit objects.
/// </summary>
internal class HitCirclePatternGenerator : LegacyPatternGenerator
{
public PatternType StairType { get; private set; }
private readonly PatternType convertType;
public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
public HitCirclePatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
double density, PatternType lastStair)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
@@ -114,10 +117,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
// If we convert to 7K + 1, let's not overload the special key
&& (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
// If we convert to 7K + 1, let's not overload the special key
&& (TotalColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
{
// Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
int column = RandomStart + TotalColumns - lastColumn - 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 System;
using System.Linq;
using JetBrains.Annotations;
@@ -15,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <summary>
/// A pattern generator for legacy hit objects.
/// </summary>
internal abstract class PatternGenerator : Patterns.PatternGenerator
internal abstract class LegacyPatternGenerator : PatternGenerator
{
/// <summary>
/// The column index at which to start generating random notes.
@@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
protected readonly LegacyRandom Random;
protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
protected LegacyPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
: base(hitObject, beatmap, totalColumns, previousPattern)
{
ArgumentNullException.ThrowIfNull(random);
@@ -96,8 +94,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (conversionDifficulty != null)
return conversionDifficulty.Value;
HitObject lastObject = Beatmap.HitObjects.LastOrDefault();
HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
HitObject? lastObject = Beatmap.HitObjects.LastOrDefault();
HitObject? firstObject = Beatmap.HitObjects.FirstOrDefault();
// Drain time in seconds
int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000);
@@ -132,13 +130,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="nextColumn">A function to retrieve the next column. If null, a randomisation scheme will be used.</param>
/// <param name="validation">A function to perform additional validation checks to determine if a column is a valid candidate for a <see cref="HitObject"/>.</param>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns">TotalColumns</see> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns">TotalColumns</see> is used.</param>
/// <param name="patterns">A list of patterns for which the validity of a column should be checked against.
/// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param>
/// <returns>A column which has passed the <paramref name="validation"/> check and for which there are no
/// <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns>
/// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception>
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int> nextColumn = null, [InstantHandle] Func<int, bool> validation = null,
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int>? nextColumn = null, [InstantHandle] Func<int, bool>? validation = null,
params Pattern[] patterns)
{
lowerBound ??= RandomStart;
@@ -189,7 +187,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
/// <summary>
@@ -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 System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
/// <summary>
/// A simple generator which, for any object, if the hitobject has an end time
/// it becomes a <see cref="HoldNote"/> or otherwise a <see cref="Note"/>.
/// </summary>
internal class PassThroughPatternGenerator : LegacyPatternGenerator
{
public PassThroughPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
}
public override IEnumerable<Pattern> Generate()
{
var positionData = HitObject as IHasXPosition;
int column = GetColumn(positionData?.X ?? 0);
var pattern = new Pattern();
if (HitObject is IHasDuration endTimeData)
{
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Duration = endTimeData.Duration,
Column = column,
Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
});
}
else
{
pattern.Add(new Note
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Column = column
});
}
yield return pattern;
}
}
}
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -19,9 +17,9 @@ using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
/// <summary>
/// A pattern generator for IHasDistance hit objects.
/// Converter for legacy "Slider" hit objects.
/// </summary>
internal class PathObjectPatternGenerator : PatternGenerator
internal class SliderPatternGenerator : LegacyPatternGenerator
{
public readonly int StartTime;
public readonly int EndTime;
@@ -30,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private PatternType convertType;
public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
public SliderPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
convertType = PatternType.None;
@@ -484,9 +482,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to retrieve node samples at.</param>
private IList<IList<HitSampleInfo>> nodeSamplesAt(int time)
private IList<IList<HitSampleInfo>>? nodeSamplesAt(int time)
{
if (!(HitObject is IHasPathWithRepeats curveData))
if (HitObject is not IHasPathWithRepeats curveData)
return null;
int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
@@ -12,12 +12,15 @@ using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class EndTimeObjectPatternGenerator : PatternGenerator
/// <summary>
/// Converter for legacy "Spinner" hit objects.
/// </summary>
internal class SpinnerPatternGenerator : LegacyPatternGenerator
{
private readonly int endTime;
private readonly PatternType convertType;
public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
public SpinnerPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern, totalColumns)
{
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using osu.Game.Rulesets.Mania.Objects;
@@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
/// </summary>
internal class Pattern
{
private List<ManiaHitObject> hitObjects;
private HashSet<int> containedColumns;
private List<ManiaHitObject>? hitObjects;
private HashSet<int>? containedColumns;
/// <summary>
/// All the hit objects contained in this pattern.
@@ -72,6 +71,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
containedColumns?.Clear();
}
[MemberNotNull(nameof(hitObjects), nameof(containedColumns))]
private void prepareStorage()
{
hitObjects ??= new List<ManiaHitObject>();
@@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime;
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
base.UpdateTimeAndPosition(result);
var result = base.UpdateTimeAndPosition(screenSpacePosition, fallbackTime);
if (PlacementActive == PlacementState.Active)
{
@@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (result.Time is double startTime)
originalStartTime = HitObject.StartTime = startTime;
}
return result;
}
}
}
@@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private EditorBeatmap? editorBeatmap { get; set; }
[Resolved]
private IPositionSnapProvider? positionSnapProvider { get; set; }
private ManiaHitObjectComposer? positionSnapProvider { get; set; }
private EditBodyPiece body = null!;
private EditHoldNoteEndPiece head = null!;
@@ -1,8 +1,8 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
@@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
protected new T HitObject => (T)base.HitObject;
private Column column;
[Resolved]
private ManiaHitObjectComposer? composer { get; set; }
public Column Column
private Column? column;
public Column? Column
{
get => column;
set
{
ArgumentNullException.ThrowIfNull(value);
if (value == column)
return;
@@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return true;
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
base.UpdateTimeAndPosition(result);
var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
if (result.Playfield is Column col)
{
@@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (PlacementActive == PlacementState.Waiting)
Column = col;
}
return result;
}
private float getNoteHeight(Column resultPlayfield) =>
@@ -8,6 +8,7 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@@ -35,15 +36,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
};
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime)
{
base.UpdateTimeAndPosition(result);
var result = base.UpdateTimeAndPosition(screenSpacePosition, referenceTime);
if (result.Playfield != null)
{
piece.Width = result.Playfield.DrawWidth;
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
}
return result;
}
protected override bool OnMouseDown(MouseDownEvent e)
@@ -1,17 +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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
public partial class ManiaBlueprintContainer : ComposeBlueprintContainer
{
public ManiaBlueprintContainer(HitObjectComposer composer)
public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer;
public ManiaBlueprintContainer(ManiaHitObjectComposer composer)
: base(composer)
{
}
@@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var result = Composer.FindSnappedPositionAndTime(movePosition);
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
}
}
@@ -19,6 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
[Cached]
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
{
private DrawableManiaEditorRuleset drawableRuleset = null!;
@@ -64,11 +65,11 @@ namespace osu.Game.Rulesets.Mania.Edit
return;
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] objectDescriptions = objectDescription.Split(',').ToArray();
string[] objectDescriptions = objectDescription.Split(',');
for (int i = 0; i < objectDescriptions.Length; i++)
{
string[] split = objectDescriptions[i].Split('|').ToArray();
string[] split = objectDescriptions[i].Split('|');
if (split.Length != 2)
continue;
+1 -1
View File
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania
{
[Cached] // Used for touch input, see ColumnTouchInputArea.
[Cached] // Used for touch input, see Column.OnTouchDown/OnTouchUp.
public partial class ManiaInputManager : RulesetInputManager<ManiaAction>
{
public ManiaInputManager(RulesetInfo ruleset, int variant)
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
{
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
HitObjectContainer hoc = column.HitObjectContainer;
Container hocParent = (Container)hoc.Parent!;
hocParent.Remove(hoc, false);
@@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
#region LegacyBeatmapEncoder
float IHasXPosition.X => Column;
float IHasXPosition.X
{
get => Column;
set => Column = (int)value;
}
#endregion
}
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -12,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Graphics;
@@ -19,25 +21,29 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
{
private const float judgement_y_position = 160;
private const float judgement_y_position = -180f;
private RingExplosion? ringExplosion;
[Resolved]
private OsuColour colours { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
public ArgonJudgementPiece(HitResult result)
: base(result)
{
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
Y = judgement_y_position;
}
[BackgroundDependencyLoader]
private void load()
private void load(IScrollingInfo scrollingInfo)
{
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
if (Result.IsHit())
{
AddInternal(ringExplosion = new RingExplosion(Result)
@@ -47,6 +53,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
}
}
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
protected override SpriteText CreateJudgementText() =>
new OsuSpriteText
{
@@ -78,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(judgement_y_position);
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
@@ -9,7 +9,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
@@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null)
{
@@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
combo.Origin = Anchor.Centre;
combo.Y = 200;
}
if (spectatorList != null)
spectatorList.Position = new Vector2(36, -66);
})
{
new ArgonManiaComboCounter(),
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
};
}
@@ -2,13 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
@@ -23,21 +24,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
this.result = result;
this.animation = animation;
Anchor = Anchor.Centre;
Anchor = Anchor.BottomCentre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
}
private IBindable<ScrollingDirection> direction = null!;
[Resolved]
private ISkinSource skin { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
private void load(IScrollingInfo scrollingInfo)
{
float? scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value;
if (scorePosition != null)
scorePosition -= Stage.HIT_TARGET_POSITION + 150;
Y = scorePosition ?? 0;
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
InternalChild = animation.With(d =>
{
@@ -46,6 +48,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
});
}
private void onDirectionChanged()
{
float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
float finalPosition = scorePosition - absoluteHitPosition;
Y = direction.Value == ScrollingDirection.Up ? -finalPosition : finalPosition;
}
public void PlayAnimation()
{
(animation as IFramedAnimation)?.GotoFrame(0);
@@ -15,7 +15,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
@@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null)
{
@@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
combo.Origin = Anchor.Centre;
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
})
{
new LegacyManiaComboCounter(),
new SpectatorList(),
};
}
+34 -7
View File
@@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
@@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
private DrawablePool<PoolableHitExplosion> hitExplosionPool;
private DrawablePool<PoolableHitExplosion> hitExplosionPool = null!;
private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
private GameplaySampleTriggerSource sampleTriggerSource;
private GameplaySampleTriggerSource sampleTriggerSource = null!;
/// <summary>
/// Whether this is a special (ie. scratch) column.
@@ -67,11 +66,15 @@ namespace osu.Game.Rulesets.Mania.UI
Width = COLUMN_WIDTH;
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both };
HitObjectArea = new ColumnHitObjectArea
{
RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer,
};
}
[Resolved]
private ISkinSource skin { get; set; }
private ISkinSource skin { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(GameHost host)
@@ -132,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI
base.Dispose(isDisposing);
if (skin != null)
if (skin.IsNotNull())
skin.SourceChanged -= onSourceChanged;
}
@@ -180,5 +183,29 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
#region Touch Input
[Resolved]
private ManiaInputManager? maniaInputManager { get; set; }
private int touchActivationCount;
protected override bool OnTouchDown(TouchDownEvent e)
{
maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
touchActivationCount++;
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
touchActivationCount--;
if (touchActivationCount == 0)
maniaInputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
}
#endregion
}
}
@@ -3,13 +3,12 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public partial class ColumnHitObjectArea : HitObjectArea
public partial class ColumnHitObjectArea : HitPositionPaddedContainer
{
public readonly Container Explosions;
@@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private readonly Drawable hitTarget;
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
: base(hitObjectContainer)
protected override Container<Drawable> Content => content;
private readonly Container content;
public ColumnHitObjectArea()
{
AddRangeInternal(new[]
{
UnderlayElements = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
{
RelativeSizeAxes = Axes.X,
Depth = 1
},
content = new Container
{
RelativeSizeAxes = Axes.Both,
},
Explosions = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
}
});
}
@@ -1,52 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public partial class HitObjectArea : SkinReloadableDrawable
public partial class HitPositionPaddedContainer : Container
{
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
public readonly HitObjectContainer HitObjectContainer;
public HitObjectArea(HitObjectContainer hitObjectContainer)
{
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer = hitObjectContainer
};
}
[Resolved]
private ISkinSource skin { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
Direction.BindTo(scrollingInfo.Direction);
Direction.BindValueChanged(onDirectionChanged, true);
Direction.BindValueChanged(_ => UpdateHitPosition(), true);
skin.SourceChanged += onSkinChanged;
}
protected override void SkinChanged(ISkinSource skin)
{
base.SkinChanged(skin);
UpdateHitPosition();
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
UpdateHitPosition();
}
private void onSkinChanged() => UpdateHitPosition();
protected virtual void UpdateHitPosition()
{
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? Stage.HIT_TARGET_POSITION;
@@ -54,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
? new MarginPadding { Top = hitPosition }
: new MarginPadding { Bottom = hitPosition };
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skin.IsNotNull())
skin.SourceChanged -= onSkinChanged;
}
}
}
@@ -0,0 +1,75 @@
// 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.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
public partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
{
private const float judgement_y_position = -180f;
private IBindable<ScrollingDirection> direction = null!;
public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
}
private void onDirectionChanged() => Y = direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position;
protected override void LoadComplete()
{
base.LoadComplete();
JudgementText.Font = JudgementText.Font.With(size: 25);
}
public override void PlayAnimation()
{
switch (Result)
{
case HitResult.None:
this.FadeOutFromOne(800);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(direction.Value == ScrollingDirection.Up ? -judgement_y_position : judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
this.FadeOutFromOne(800);
break;
default:
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
break;
}
}
}
}
@@ -3,51 +3,32 @@
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public partial class DrawableManiaJudgement : DrawableJudgement
{
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private IBindable<ScrollingDirection> direction;
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
public DefaultManiaJudgementPiece(HitResult result)
: base(result)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
JudgementText.Font = JudgementText.Font.With(size: 25);
}
public override void PlayAnimation()
{
switch (Result)
{
case HitResult.None:
case HitResult.Miss:
base.PlayAnimation();
break;
default:
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
break;
}
}
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => onDirectionChanged(), true);
}
private void onDirectionChanged()
{
Anchor = direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
Origin = Anchor.Centre;
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
}
}
@@ -32,7 +32,6 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI
{
[Cached]
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
{
/// <summary>
@@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable<BarLine> BarLines;
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1;
protected override bool RelativeScaleBeatLengths => true;
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
@@ -110,8 +111,6 @@ namespace osu.Game.Rulesets.Mania.UI
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
KeyBindingInputManager.Add(new ManiaTouchInputArea());
}
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
@@ -162,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// <returns>The scroll time.</returns>
public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(this);
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);
@@ -1,17 +1,63 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
public ManiaPlayfieldAdjustmentContainer()
protected override Container<Drawable> Content { get; }
private readonly DrawSizePreservingFillContainer scalingContainer;
private readonly DrawableManiaRuleset drawableManiaRuleset;
public ManiaPlayfieldAdjustmentContainer(DrawableManiaRuleset drawableManiaRuleset)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
this.drawableManiaRuleset = drawableManiaRuleset;
InternalChild = scalingContainer = new DrawSizePreservingFillContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = Content = new Container
{
RelativeSizeAxes = Axes.Both,
}
};
}
protected override void Update()
{
base.Update();
float aspectRatio = DrawWidth / DrawHeight;
bool isPortrait = aspectRatio < 1f;
if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1)
{
// Scale playfield up by 25% to become playable on mobile devices,
// and leave a 10% horizontal gap if the playfield is scaled down due to being too wide.
const float base_scale = 1.25f;
const float base_width = 768f / base_scale;
const float side_gap = 0.9f;
scalingContainer.Strategy = DrawSizePreservationStrategy.Maximum;
float stageWidth = drawableManiaRuleset.Playfield.Stages[0].DrawWidth;
scalingContainer.TargetDrawSize = new Vector2(1024, base_width * Math.Max(stageWidth / aspectRatio / (base_width * side_gap), 1f));
}
else
{
scalingContainer.Strategy = DrawSizePreservationStrategy.Minimum;
scalingContainer.Scale = new Vector2(1f);
scalingContainer.Size = new Vector2(1f);
scalingContainer.TargetDrawSize = new Vector2(1024, 768);
}
}
}
}
@@ -1,199 +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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
/// <summary>
/// An overlay that captures and displays osu!mania mouse and touch input.
/// </summary>
public partial class ManiaTouchInputArea : VisibilityContainer
{
// visibility state affects our child. we always want to handle input.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
[SettingSource("Spacing", "The spacing between receptors.")]
public BindableFloat Spacing { get; } = new BindableFloat(10)
{
Precision = 1,
MinValue = 0,
MaxValue = 100,
};
[SettingSource("Opacity", "The receptor opacity.")]
public BindableFloat Opacity { get; } = new BindableFloat(1)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 1
};
[Resolved]
private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
private GridContainer gridContainer = null!;
public ManiaTouchInputArea()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
RelativeSizeAxes = Axes.Both;
Height = 0.5f;
}
[BackgroundDependencyLoader]
private void load()
{
List<Drawable> receptorGridContent = new List<Drawable>();
List<Dimension> receptorGridDimensions = new List<Dimension>();
bool first = true;
foreach (var stage in drawableRuleset.Playfield.Stages)
{
foreach (var column in stage.Columns)
{
if (!first)
{
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
}
receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
receptorGridDimensions.Add(new Dimension());
first = false;
}
}
InternalChild = gridContainer = new GridContainer
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Content = new[] { receptorGridContent.ToArray() },
ColumnDimensions = receptorGridDimensions.ToArray()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
Show();
return true;
}
protected override void PopIn()
{
gridContainer.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
gridContainer.FadeOut(300);
}
public partial class ColumnInputReceptor : CompositeDrawable
{
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
private readonly Box highlightOverlay;
[Resolved]
private ManiaInputManager? inputManager { get; set; }
private bool isPressed;
public ColumnInputReceptor()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.15f,
},
highlightOverlay = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
}
}
};
}
protected override bool OnTouchDown(TouchDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnTouchUp(TouchUpEvent e)
{
updateButton(false);
}
private void updateButton(bool press)
{
if (press == isPressed)
return;
isPressed = press;
if (press)
{
inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
}
else
{
inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
}
}
}
private partial class Gutter : Drawable
{
public readonly IBindable<float> Spacing = new Bindable<float>();
public Gutter()
{
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
}
}
}
}
+8 -12
View File
@@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI
Width = 1366, // Bar lines should only be masked on the vertical axis
BypassAutoSizeAxes = Axes.Both,
Masking = true,
Child = barLineContainer = new HitObjectArea(HitObjectContainer)
Child = barLineContainer = new HitPositionPaddedContainer
{
Name = "Bar lines",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Child = HitObjectContainer,
}
},
columnFlow = new ColumnFlow<Column>(definition)
@@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI
{
RelativeSizeAxes = Axes.Both
},
judgements = new JudgementContainer<DrawableManiaJudgement>
new HitPositionPaddedContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Y = HIT_TARGET_POSITION + 150
Child = judgements = new JudgementContainer<DrawableManiaJudgement>
{
RelativeSizeAxes = Axes.Both,
},
},
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
}
@@ -214,13 +216,7 @@ namespace osu.Game.Rulesets.Mania.UI
return;
judgements.Clear(false);
judgements.Add(judgementPooler.Get(result.Type, j =>
{
j.Apply(result, judgedObject);
j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre;
})!);
judgements.Add(judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject))!);
}
protected override void Update()
@@ -0,0 +1,15 @@
// 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 Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}
@@ -1,16 +1,15 @@
// 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.iOS;
using osu.Game.Tests;
using UIKit;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
public static class Application
public static class Program
{
public static void Main(string[] args)
{
GameApplication.Main(new OsuTestBrowser());
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
@@ -10,7 +10,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
@@ -261,6 +263,163 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
}
[Test]
public void TestQuickDeleteOnUnselectedControlPointOnlyRemovesThatControlPoint()
{
var slider = new Slider
{
StartTime = 0,
Position = new Vector2(100, 100),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint { Type = PathType.LINEAR },
new PathControlPoint(new Vector2(100, 0)),
new PathControlPoint(new Vector2(100)),
new PathControlPoint(new Vector2(0, 100))
}
}
};
AddStep("add slider", () => EditorBeatmap.Add(slider));
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("select second node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddStep("also select third node", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("quick-delete fourth node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(3));
InputManager.Click(MouseButton.Middle);
});
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
AddUntilStep("slider path has 3 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(3));
}
[Test]
public void TestQuickDeleteOnSelectedControlPointRemovesEntireSelection()
{
var slider = new Slider
{
StartTime = 0,
Position = new Vector2(100, 100),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint { Type = PathType.LINEAR },
new PathControlPoint(new Vector2(100, 0)),
new PathControlPoint(new Vector2(100)),
new PathControlPoint(new Vector2(0, 100))
}
}
};
AddStep("add slider", () => EditorBeatmap.Add(slider));
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("select second node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddStep("also select third node", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("quick-delete second node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
InputManager.Click(MouseButton.Middle);
});
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
AddUntilStep("slider path has 2 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(2));
}
[Test]
public void TestSliderDragMarkerDoesNotBlockControlPointContextMenu()
{
var slider = new Slider
{
StartTime = 0,
Position = new Vector2(100, 100),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint { Type = PathType.LINEAR },
new PathControlPoint(new Vector2(50, 100)),
new PathControlPoint(new Vector2(145, 100)),
},
ExpectedDistance = { Value = 162.62 }
},
};
AddStep("add slider", () => EditorBeatmap.Add(slider));
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("select last node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().Last());
InputManager.Click(MouseButton.Left);
});
AddStep("right click node", () => InputManager.Click(MouseButton.Right));
AddUntilStep("context menu open", () => this.ChildrenOfType<ContextMenuContainer>().Single().ChildrenOfType<Menu>().All(m => m.State == MenuState.Open));
}
[Test]
public void TestSliderDragMarkerBlocksSelectionOfObjectsUnderneath()
{
var firstSlider = new Slider
{
StartTime = 0,
Position = new Vector2(10, 50),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
var secondSlider = new Slider
{
StartTime = 500,
Position = new Vector2(200, 0),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(-100, 100))
}
}
};
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
AddStep("select second slider", () => EditorBeatmap.SelectedHitObjects.Add(secondSlider));
AddStep("move to marker", () =>
{
var marker = this.ChildrenOfType<SliderEndDragMarker>().First();
var position = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
InputManager.MoveMouseTo(position);
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("second slider still selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondSlider));
}
private ComposeBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
@@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModRelax : OsuModTestScene
{
private readonly HitCircle hitObject;
private readonly HitWindows hitWindows = new OsuHitWindows();
public TestSceneOsuModRelax()
{
hitWindows.SetDifficulty(9);
hitObject = new HitCircle
{
StartTime = 1000,
Position = new Vector2(100, 100),
HitWindows = hitWindows
};
}
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail);
[Test]
public void TestRelax() => CreateModTest(new ModTestData
{
Mod = new OsuModRelax(),
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject> { hitObject }
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2()),
new OsuReplayFrame(hitObject.StartTime, hitObject.Position),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
});
[Test]
public void TestRelaxLeniency() => CreateModTest(new ModTestData
{
Mod = new OsuModRelax(),
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject> { hitObject }
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long
new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)),
new OsuReplayFrame(hitObject.StartTime, new Vector2(0)),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
});
protected partial class ModRelaxTestPlayer : ModTestPlayer
{
private readonly ModTestData currentTestData;
public ModRelaxTestPlayer(ModTestData data, bool allowFail)
: base(data, allowFail)
{
currentTestData = data;
}
protected override void PrepareReplay()
{
// We need to set IsLegacyScore to true otherwise the mod assumes that presses are already embedded into the replay
DrawableRuleset?.SetReplayScore(new Score
{
Replay = new Replay { Frames = currentTestData.ReplayFrames! },
ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" }, IsLegacyScore = true, Mods = new Mod[] { new OsuModRelax() } },
});
DrawableRuleset?.SetRecordTarget(Score);
}
}
}
}
@@ -24,11 +24,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
}
[Test]
public void TestMissTail() => CreateModTest(new ModTestData
[TestCase(true)]
[TestCase(false)]
public void TestMissTail(bool tailMiss) => CreateModTest(new ModTestData
{
Mod = new OsuModSuddenDeath(),
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
Mod = new OsuModSuddenDeath
{
FailOnSliderTail = { Value = tailMiss }
},
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(tailMiss),
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@@ -17,6 +18,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Framework.Testing.Input;
using osu.Game.Audio;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@@ -103,6 +105,23 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
}
[Test]
public void TestRotation()
{
createTest(() =>
{
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true, enableRotation: true);
var legacyCursorTrail = new LegacyRotatingCursorTrail(skinContainer)
{
NewPartScale = new Vector2(10)
};
skinContainer.Child = legacyCursorTrail;
return skinContainer;
});
}
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
{
Clear();
@@ -121,12 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly IRenderer renderer;
private readonly bool provideMiddle;
private readonly bool provideCursor;
private readonly bool enableRotation;
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true)
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true, bool enableRotation = false)
{
this.renderer = renderer;
this.provideMiddle = provideMiddle;
this.provideCursor = provideCursor;
this.enableRotation = enableRotation;
RelativeSizeAxes = Axes.Both;
}
@@ -152,7 +173,19 @@ namespace osu.Game.Rulesets.Osu.Tests
public ISample GetSample(ISampleInfo sampleInfo) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
switch (lookup)
{
case OsuSkinConfiguration osuLookup:
if (osuLookup == OsuSkinConfiguration.CursorTrailRotate)
return SkinUtils.As<TValue>(new BindableBool(enableRotation));
break;
}
return null;
}
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
@@ -185,5 +218,19 @@ namespace osu.Game.Rulesets.Osu.Tests
MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
}
}
private partial class LegacyRotatingCursorTrail : LegacyCursorTrail
{
public LegacyRotatingCursorTrail([NotNull] ISkin skin)
: base(skin)
{
}
protected override void Update()
{
base.Update();
PartRotation += (float)(Time.Elapsed * 0.1);
}
}
}
}
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public partial class GridPlacementBlueprint : PlacementBlueprint
{
[Resolved]
private HitObjectComposer? hitObjectComposer { get; set; }
private OsuHitObjectComposer? hitObjectComposer { get; set; }
private OsuGridToolboxGroup gridToolboxGroup = null!;
private Vector2 originalOrigin;
@@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
base.OnDragEnd(e);
}
public override SnapType SnapType => ~SnapType.GlobalGrids;
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
if (State.Value == Visibility.Hidden)
return;
return new SnapResult(screenSpacePosition, fallbackTime);
var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
var pos = ToLocalSpace(result.ScreenSpacePosition);
@@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos);
}
}
return result;
}
protected override void PopOut()
@@ -1,10 +1,15 @@
// 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.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
@@ -15,12 +20,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
private readonly HitCirclePiece circlePiece;
[Resolved]
private OsuHitObjectComposer? composer { get; set; }
[Resolved]
private EditorClock? editorClock { get; set; }
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
public HitCirclePlacementBlueprint()
: base(new HitCircle())
{
InternalChild = circlePiece = new HitCirclePiece();
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
}
protected override void LoadComplete()
{
base.LoadComplete();
@@ -45,10 +64,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
base.UpdateTimeAndPosition(result);
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
result = gridSnapResult;
result ??= new SnapResult(screenSpacePosition, fallbackTime);
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
return result;
}
}
}
@@ -9,6 +9,7 @@ using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -20,6 +21,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -32,7 +34,7 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
where T : OsuHitObject, IHasPath
where T : OsuHitObject, IHasPath, IHasSliderVelocity
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside the playfield.
@@ -48,11 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<List<PathControlPoint>> SplitControlPointsRequested;
[Resolved(CanBeNull = true)]
private IPositionSnapProvider positionSnapProvider { get; set; }
[CanBeNull]
private OsuHitObjectComposer positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
public PathControlPointVisualiser(T hitObject, bool allowSelection)
{
this.hitObject = hitObject;
@@ -67,6 +72,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
};
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
}
protected override void LoadComplete()
{
base.LoadComplete();
@@ -137,11 +148,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// <summary>
/// Delete all visually selected <see cref="PathControlPoint"/>s.
/// </summary>
/// <returns></returns>
/// <returns>Whether any change actually took place.</returns>
public bool DeleteSelected()
{
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
if (!Delete(toRemove))
return false;
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
/// <summary>
/// Delete the specified <see cref="PathControlPoint"/>s.
/// </summary>
/// <returns>Whether any change actually took place.</returns>
public bool Delete(List<PathControlPoint> toRemove)
{
// Ensure that there are any points to be deleted
if (toRemove.Count == 0)
return false;
@@ -149,11 +176,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
changeHandler?.BeginChange();
RemoveControlPointsRequested?.Invoke(toRemove);
changeHandler?.EndChange();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
@@ -422,12 +444,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime);
result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null);
if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult)
result = gridSnapResult;
result ??= new SnapResult(newHeadPosition, oldStartTime);
Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position;
hitObject.Position += movementDelta;
hitObject.StartTime = result?.Time ?? hitObject.StartTime;
hitObject.StartTime = result.Time ?? hitObject.StartTime;
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
{
@@ -442,7 +469,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
{
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition));
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
@@ -10,6 +10,7 @@ using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
@@ -76,6 +77,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnDragEnd(e);
}
protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left;
protected override bool OnClick(ClickEvent e) => e.Button == MouseButton.Left;
private void updateState()
{
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;
@@ -5,10 +5,12 @@ 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.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -25,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public new Slider HitObject => (Slider)base.HitObject;
[Resolved]
private OsuHitObjectComposer? composer { get; set; }
private SliderBodyPiece bodyPiece = null!;
private HitCirclePiece headCirclePiece = null!;
private HitCirclePiece tailCirclePiece = null!;
@@ -40,15 +45,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int currentSegmentLength;
private bool usingCustomSegmentType;
[Resolved]
private IPositionSnapProvider? positionSnapProvider { get; set; }
[Resolved]
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
[Resolved]
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
[Resolved]
private EditorClock? editorClock { get; set; }
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
@@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config)
{
InternalChildren = new Drawable[]
{
@@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
};
state = SliderPlacementState.Initial;
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
}
protected override void LoadComplete()
@@ -106,9 +114,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
public override void UpdateTimeAndPosition(SnapResult result)
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{
base.UpdateTimeAndPosition(result);
var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
result = gridSnapResult;
result ??= new SnapResult(screenSpacePosition, fallbackTime);
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
switch (state)
{
@@ -131,6 +145,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
updateCursor();
break;
}
return result;
}
protected override bool OnMouseDown(MouseDownEvent e)
@@ -375,7 +391,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private Vector2 getCursorPosition()
{
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
SnapResult? result = null;
var mousePosition = inputManager.CurrentState.Mouse.Position;
if (state != SliderPlacementState.ControlPoints)
{
result ??= composer?.TrySnapToNearbyObjects(mousePosition);
result ??= composer?.TrySnapToDistanceGrid(mousePosition);
}
result ??= composer?.TrySnapToPositionGrid(mousePosition);
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
@@ -408,7 +434,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (state == SliderPlacementState.Drawing)
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
else
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance((float)HitObject.Path.CalculatedDistance, HitObject.StartTime, HitObject) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
@@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (hoveredControlPoint == null)
return false;
hoveredControlPoint.IsSelected.Value = true;
ControlPointVisualiser?.DeleteSelected();
if (hoveredControlPoint.IsSelected.Value)
ControlPointVisualiser?.DeleteSelected();
else
ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]);
return true;
}
@@ -271,9 +274,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
else
{
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
}
@@ -623,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0))
return true;
if (ControlPointVisualiser == null)
@@ -1,6 +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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
@@ -8,16 +14,27 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuBlueprintContainer : ComposeBlueprintContainer
{
public OsuBlueprintContainer(HitObjectComposer composer)
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer;
public OsuBlueprintContainer(OsuHitObjectComposer composer)
: base(composer)
{
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
}
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
@@ -36,5 +53,68 @@ namespace osu.Game.Rulesets.Osu.Edit
return base.CreateHitObjectBlueprintFor(hitObject);
}
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
for (int i = 0; i < blueprints.Count; i++)
{
if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions))
return true;
}
// if no positional snapping could be performed, try unrestricted snapping from the earliest
// item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
var referenceBlueprint = blueprints.First().blueprint;
// Retrieve a snapped position.
var result = Composer.TrySnapToNearbyObjects(movePosition);
result ??= Composer.TrySnapToDistanceGrid(movePosition, limitedDistanceSnap.Value ? referenceBlueprint.Item.StartTime : null);
if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult)
result = gridSnapResult;
result ??= new SnapResult(movePosition, null);
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
/// <summary>
/// Check for positional snap for given blueprint.
/// </summary>
/// <param name="blueprint">The blueprint to check for snapping.</param>
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
/// <returns>Whether an object to snap to was found.</returns>
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<HitObject> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
{
var currentPositions = blueprint.ScreenSpaceSnapPoints;
for (int i = 0; i < originalPositions.Length; i++)
{
Vector2 originalPosition = originalPositions[i];
var testPosition = originalPosition + distanceTravelled;
var positionalResult = Composer.TrySnapToNearbyObjects(testPosition);
if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue;
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
// attempt to move the objects, and apply any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(blueprint, delta)))
{
ApplySnapResultTime(positionalResult, blueprint.Item.StartTime);
return true;
}
}
return false;
}
}
}
@@ -5,6 +5,7 @@
using JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
@@ -12,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
: base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1)
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null, [CanBeNull] IHasSliderVelocity sliderVelocitySource = null)
: base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1, sliderVelocitySource)
{
Masking = true;
}
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
return actualDistance / expectedDistance;
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
@@ -22,6 +23,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
@@ -31,6 +33,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
[Cached]
public partial class OsuHitObjectComposer : HitObjectComposer<OsuHitObject>
{
public OsuHitObjectComposer(Ruleset ruleset)
@@ -53,9 +56,14 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
protected override IEnumerable<Drawable> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
.Append(new DrawableTernaryButton
{
Current = rectangularGridSnapToggle,
Description = "Grid Snap",
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap },
})
.Concat(DistanceSnapProvider.CreateTernaryButtons());
private BindableList<HitObject> selectedHitObjects;
@@ -173,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit
return;
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] splitDescription = objectDescription.Split(',').ToArray();
string[] splitDescription = objectDescription.Split(',');
for (int i = 0; i < splitDescription.Length; i++)
{
@@ -217,56 +225,56 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
[CanBeNull]
public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition, double? fallbackTime = null)
{
if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
{
// In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
//
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
snapResult.Time = distanceSnappedTime;
}
if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return null;
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
return snapResult;
}
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
// In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
//
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
snapResult.Time = Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)
? distanceSnappedTime
: fallbackTime;
if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
return snapResult;
}
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
result.Time = time;
}
}
[CanBeNull]
public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null)
{
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
return null;
if (snapType.HasFlag(SnapType.GlobalGrids))
{
if (rectangularGridSnapToggle.Value == TernaryState.True)
{
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
}
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
[CanBeNull]
public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null)
{
if (rectangularGridSnapToggle.Value != TernaryState.True)
return null;
result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos);
}
}
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition));
return result;
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
return new SnapResult(positionSnapGrid.ToScreenSpace(pos), fallbackTime, playfield);
}
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
@@ -399,22 +407,26 @@ namespace osu.Game.Rulesets.Osu.Edit
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(targetOffset);
int sourceIndex = -1;
int positionSourceObjectIndex = -1;
IHasSliderVelocity sliderVelocitySource = null;
for (int i = 0; i < EditorBeatmap.HitObjects.Count; i++)
{
if (!sourceSelector(EditorBeatmap.HitObjects[i]))
break;
sourceIndex = i;
positionSourceObjectIndex = i;
if (EditorBeatmap.HitObjects[i] is IHasSliderVelocity hasSliderVelocity)
sliderVelocitySource = hasSliderVelocity;
}
if (sourceIndex == -1)
if (positionSourceObjectIndex == -1)
return null;
HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex];
HitObject sourceObject = EditorBeatmap.HitObjects[positionSourceObjectIndex];
int targetIndex = sourceIndex + targetOffset;
int targetIndex = positionSourceObjectIndex + targetOffset;
HitObject targetObject = null;
// Keep advancing the target object while its start time falls before the end time of the source object
@@ -435,7 +447,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (sourceObject is Spinner)
return null;
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject);
return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject, sliderVelocitySource);
}
}
}

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