mirror of
https://github.com/ppy/osu.git
synced 2025-02-06 23:12:54 +08:00
Merge branch 'master' into pp-dev
This commit is contained in:
commit
aeca37c230
@ -51,8 +51,11 @@ dotnet_diagnostic.IDE1006.severity = warning
|
|||||||
# Too many noisy warnings for parsing/formatting numbers
|
# Too many noisy warnings for parsing/formatting numbers
|
||||||
dotnet_diagnostic.CA1305.severity = none
|
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
|
# CA1507: Use nameof to express symbol names
|
||||||
# Flaggs serialization name attributes
|
# Flags serialization name attributes
|
||||||
dotnet_diagnostic.CA1507.severity = suggestion
|
dotnet_diagnostic.CA1507.severity = suggestion
|
||||||
|
|
||||||
# CA1806: Do not ignore method results
|
# CA1806: Do not ignore method results
|
||||||
|
@ -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.
|
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
|
## Developing a custom ruleset
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
<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 Vector2 Position { get; set; }
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
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>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<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 Vector2 Position { get; set; }
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
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>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||||
|
@ -9,9 +9,9 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.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>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1206.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.115.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -13,7 +13,6 @@ using Android.Graphics;
|
|||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using osu.Framework.Android;
|
using osu.Framework.Android;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using Debug = System.Diagnostics.Debug;
|
using Debug = System.Diagnostics.Debug;
|
||||||
using Uri = Android.Net.Uri;
|
using Uri = Android.Net.Uri;
|
||||||
@ -50,9 +49,23 @@ namespace osu.Android
|
|||||||
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
||||||
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
||||||
|
|
||||||
private OsuGameAndroid game = null!;
|
private readonly OsuGameAndroid game;
|
||||||
|
|
||||||
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
|
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)
|
protected override void OnCreate(Bundle? savedInstanceState)
|
||||||
{
|
{
|
||||||
@ -95,25 +108,38 @@ namespace osu.Android
|
|||||||
|
|
||||||
private void handleIntent(Intent? intent)
|
private void handleIntent(Intent? intent)
|
||||||
{
|
{
|
||||||
switch (intent?.Action)
|
if (intent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (intent.Action)
|
||||||
{
|
{
|
||||||
case Intent.ActionDefault:
|
case Intent.ActionDefault:
|
||||||
if (intent.Scheme == ContentResolver.SchemeContent)
|
if (intent.Scheme == ContentResolver.SchemeContent)
|
||||||
handleImportFromUris(intent.Data.AsNonNull());
|
{
|
||||||
|
if (intent.Data != null)
|
||||||
|
handleImportFromUris(intent.Data);
|
||||||
|
}
|
||||||
else if (osu_url_schemes.Contains(intent.Scheme))
|
else if (osu_url_schemes.Contains(intent.Scheme))
|
||||||
game.HandleLink(intent.DataString);
|
{
|
||||||
|
if (intent.DataString != null)
|
||||||
|
game.HandleLink(intent.DataString);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Intent.ActionSend:
|
case Intent.ActionSend:
|
||||||
case Intent.ActionSendMultiple:
|
case Intent.ActionSendMultiple:
|
||||||
{
|
{
|
||||||
|
if (intent.ClipData == null)
|
||||||
|
break;
|
||||||
|
|
||||||
var uris = new List<Uri>();
|
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);
|
var item = intent.ClipData.GetItemAt(i);
|
||||||
if (content != null)
|
if (item?.Uri != null)
|
||||||
uris.Add(content.Uri.AsNonNull());
|
uris.Add(item.Uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImportFromUris(uris.ToArray());
|
handleImportFromUris(uris.ToArray());
|
||||||
|
@ -51,12 +51,9 @@ namespace osu.Desktop
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
|
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
private IBindable<DiscordRichPresenceMode> privacyMode = null!;
|
||||||
private OsuConfigManager config { get; set; } = null!;
|
private IBindable<UserStatus> userStatus = null!;
|
||||||
|
private IBindable<UserActivity?> userActivity = 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 readonly RichPresence presence = new RichPresence
|
private readonly RichPresence presence = new RichPresence
|
||||||
{
|
{
|
||||||
@ -71,8 +68,12 @@ namespace osu.Desktop
|
|||||||
private IBindable<APIUser>? user;
|
private IBindable<APIUser>? user;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[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)
|
client = new DiscordRpcClient(client_id)
|
||||||
{
|
{
|
||||||
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
|
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
|
||||||
@ -105,21 +106,11 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
|
||||||
|
|
||||||
user = api.LocalUser.GetBoundCopy();
|
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());
|
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
status.BindValueChanged(_ => schedulePresenceUpdate());
|
userStatus.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
activity.BindValueChanged(_ => schedulePresenceUpdate());
|
userActivity.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
||||||
|
|
||||||
multiplayerClient.RoomUpdated += onRoomUpdated;
|
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||||
@ -151,13 +142,13 @@ namespace osu.Desktop
|
|||||||
if (!client.IsInitialized)
|
if (!client.IsInitialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
if (!api.IsLoggedIn || userStatus.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
||||||
{
|
{
|
||||||
client.ClearPresence();
|
client.ClearPresence();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
|
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || userStatus.Value == UserStatus.DoNotDisturb;
|
||||||
|
|
||||||
updatePresence(hideIdentifiableInformation);
|
updatePresence(hideIdentifiableInformation);
|
||||||
client.SetPresence(presence);
|
client.SetPresence(presence);
|
||||||
@ -170,12 +161,12 @@ namespace osu.Desktop
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// user activity
|
// user activity
|
||||||
if (activity.Value != null)
|
if (userActivity.Value != null)
|
||||||
{
|
{
|
||||||
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
|
presence.State = clampLength(userActivity.Value.GetStatus(hideIdentifiableInformation));
|
||||||
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
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[]
|
presence.Buttons = new[]
|
||||||
{
|
{
|
||||||
|
@ -67,7 +67,12 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
try
|
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))
|
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
||||||
return stableInstallPath;
|
return stableInstallPath;
|
||||||
@ -89,9 +94,9 @@ namespace osu.Desktop
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SupportedOSPlatform("windows")]
|
[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", "");
|
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)
|
if (iconStream != null)
|
||||||
host.Window.SetIconFromStream(iconStream);
|
host.Window.SetIconFromStream(iconStream);
|
||||||
|
|
||||||
host.Window.CursorState |= CursorState.Hidden;
|
|
||||||
host.Window.Title = Name;
|
host.Window.Title = Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ namespace osu.Desktop.Windows
|
|||||||
public static class WindowsAssociationManager
|
public static class WindowsAssociationManager
|
||||||
{
|
{
|
||||||
private const string software_classes = @"Software\Classes";
|
private const string software_classes = @"Software\Classes";
|
||||||
|
private const string software_registered_applications = @"Software\RegisteredApplications";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sub key for setting the icon.
|
/// 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,
|
/// 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.
|
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||||
/// </summary>
|
/// </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 =
|
private static readonly FileAssociation[] file_associations =
|
||||||
{
|
{
|
||||||
@ -56,14 +61,13 @@ namespace osu.Desktop.Windows
|
|||||||
/// Installs file and URI associations.
|
/// Installs file and URI associations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <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>
|
/// </remarks>
|
||||||
public static void InstallAssociations()
|
public static void InstallAssociations()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
updateAssociations();
|
updateAssociations();
|
||||||
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
|
||||||
NotifyShellUpdate();
|
NotifyShellUpdate();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -76,17 +80,13 @@ namespace osu.Desktop.Windows
|
|||||||
/// Updates associations with latest definitions.
|
/// Updates associations with latest definitions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <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>
|
/// </remarks>
|
||||||
public static void UpdateAssociations()
|
public static void UpdateAssociations()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
updateAssociations();
|
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();
|
NotifyShellUpdate();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
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
|
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();
|
NotifyShellUpdate();
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -112,6 +120,8 @@ namespace osu.Desktop.Windows
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
application_capability.Uninstall();
|
||||||
|
|
||||||
foreach (var association in file_associations)
|
foreach (var association in file_associations)
|
||||||
association.Uninstall();
|
association.Uninstall();
|
||||||
|
|
||||||
@ -133,22 +143,16 @@ namespace osu.Desktop.Windows
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static void updateAssociations()
|
private static void updateAssociations()
|
||||||
{
|
{
|
||||||
|
application_capability.Install();
|
||||||
|
|
||||||
foreach (var association in file_associations)
|
foreach (var association in file_associations)
|
||||||
association.Install();
|
association.Install();
|
||||||
|
|
||||||
foreach (var association in uri_associations)
|
foreach (var association in uri_associations)
|
||||||
association.Install();
|
association.Install();
|
||||||
}
|
|
||||||
|
|
||||||
private static void updateDescriptions(LocalisationManager? localisation)
|
application_capability.RegisterFileAssociations(file_associations);
|
||||||
{
|
application_capability.RegisterUriAssociations(uri_associations);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Native interop
|
#region Native interop
|
||||||
@ -174,9 +178,87 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
#endregion
|
#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>
|
/// <summary>
|
||||||
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
/// 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;
|
if (classes == null) return;
|
||||||
|
|
||||||
// register a program id for the given extension
|
// 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))
|
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||||
defaultIconKey.SetValue(null, IconPath);
|
defaultIconKey.SetValue(null, iconPath);
|
||||||
|
|
||||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||||
@ -198,23 +282,25 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
using (var extensionKey = classes.CreateSubKey(Extension))
|
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||||
{
|
{
|
||||||
// set ourselves as the default program
|
// Clear out our existing default ProgramID. Default programs in Windows are handled internally by Explorer,
|
||||||
extensionKey.SetValue(null, programId);
|
// 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
|
// 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
|
// 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"))
|
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);
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
if (classes == null) return;
|
if (classes == null) return;
|
||||||
|
|
||||||
using (var programKey = classes.OpenSubKey(programId, true))
|
using (var programKey = classes.OpenSubKey(ProgramId, true))
|
||||||
programKey?.SetValue(null, description);
|
programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -227,26 +313,34 @@ namespace osu.Desktop.Windows
|
|||||||
|
|
||||||
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
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"))
|
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>
|
/// <summary>
|
||||||
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
/// "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).
|
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||||
/// </summary>
|
/// </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>
|
/// <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).
|
/// 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))
|
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))
|
// clear out old data
|
||||||
defaultIconKey.SetValue(null, IconPath);
|
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""");
|
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);
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
if (classes == null) return;
|
if (classes == null) return;
|
||||||
|
|
||||||
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||||
protocolKey?.SetValue(null, $@"URL:{description}");
|
protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Uninstall()
|
public void Uninstall()
|
||||||
{
|
{
|
||||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||||
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,9 @@
|
|||||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
|
||||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||||
<PackageReference Include="Velopack" Version="0.0.915" />
|
<PackageReference Include="Velopack" Version="0.0.1053" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Resources">
|
<ItemGroup Label="Resources">
|
||||||
<EmbeddedResource Include="lazer.ico" />
|
<EmbeddedResource Include="lazer.ico" />
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||||
<PackageReference Include="nunit" Version="3.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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
15
osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs
Normal file
15
osu.Game.Rulesets.Catch.Tests.iOS/AppDelegate.cs
Normal file
@ -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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.iOS;
|
using UIKit;
|
||||||
using osu.Game.Tests;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.Tests.iOS
|
namespace osu.Game.Rulesets.Catch.Tests.iOS
|
||||||
{
|
{
|
||||||
public static class Application
|
public static class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
GameApplication.Main(new OsuTestBrowser());
|
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
{
|
{
|
||||||
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
||||||
{
|
{
|
||||||
private JuiceStream hitObject;
|
private JuiceStream hitObject = null!;
|
||||||
|
|
||||||
private readonly ManualClock manualClock = new ManualClock();
|
private readonly ManualClock manualClock = new ManualClock();
|
||||||
|
|
||||||
@ -193,6 +191,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
|||||||
addVertexCheckStep(1, 0, times[0], positions[0]);
|
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]
|
[Test]
|
||||||
public void TestVertexResampling()
|
public void TestVertexResampling()
|
||||||
{
|
{
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.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>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -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.
|
// The SV setting may need to be changed for the current path.
|
||||||
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
||||||
|
@ -138,5 +138,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
|||||||
|
|
||||||
EditorBeatmap?.EndChange();
|
EditorBeatmap?.EndChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||||
|
{
|
||||||
|
base.UpdateHitObjectFromPath(hitObject);
|
||||||
|
|
||||||
|
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
||||||
|
EditorBeatmap?.Remove(hitObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,10 +88,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
switch (PlacementActive)
|
switch (PlacementActive)
|
||||||
{
|
{
|
||||||
case PlacementState.Waiting:
|
case PlacementState.Waiting:
|
||||||
if (!(result.Time is double snappedTime)) return;
|
|
||||||
|
|
||||||
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
|
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||||
HitObject.StartTime = snappedTime;
|
if (result.Time is double snappedTime)
|
||||||
|
HitObject.StartTime = snappedTime;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlacementState.Active:
|
case PlacementState.Active:
|
||||||
@ -107,21 +106,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
|||||||
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||||
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
|
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();
|
ApplyDefaultsToHitObject();
|
||||||
|
|
||||||
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
||||||
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
||||||
|
|
||||||
lastEditablePathId = editablePath.PathId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double positionToTime(float relativeYPosition)
|
private double positionToTime(float relativeYPosition)
|
||||||
|
@ -18,7 +18,6 @@ using osu.Game.Rulesets.Edit.Tools;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -72,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
|
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
|
||||||
|
|
||||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
protected override IEnumerable<Drawable> CreateTernaryButtons()
|
||||||
=> base.CreateTernaryButtons()
|
=> base.CreateTernaryButtons()
|
||||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||||
|
|
||||||
|
@ -159,27 +159,26 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
{
|
{
|
||||||
// Note that this implementation is shared with the osu! ruleset's implementation.
|
// Note that this implementation is shared with the osu! ruleset's implementation.
|
||||||
// If a change is made here, OsuHitObject.cs should also be updated.
|
// If a change is made here, OsuHitObject.cs should also be updated.
|
||||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
int index = lastObj?.ComboIndex ?? 0;
|
||||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 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.
|
inCurrentCombo = 0;
|
||||||
return;
|
index++;
|
||||||
}
|
indexWithOffsets += ComboOffset + 1;
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
if (lastObj != null)
|
if (lastObj != null)
|
||||||
lastObj.LastInCombo = true;
|
lastObj.LastInCombo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComboIndex = index;
|
||||||
|
ComboIndexWithOffsets = indexWithOffsets;
|
||||||
|
IndexInCurrentCombo = inCurrentCombo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
@ -210,11 +209,27 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
|
15
osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs
Normal file
15
osu.Game.Rulesets.Mania.Tests.iOS/AppDelegate.cs
Normal file
@ -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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.iOS;
|
using UIKit;
|
||||||
using osu.Game.Tests;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Tests.iOS
|
namespace osu.Game.Rulesets.Mania.Tests.iOS
|
||||||
{
|
{
|
||||||
public static class Application
|
public static class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
GameApplication.Main(new OsuTestBrowser());
|
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,9 +22,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
|||||||
|
|
||||||
[TestCase("basic")]
|
[TestCase("basic")]
|
||||||
[TestCase("zero-length-slider")]
|
[TestCase("zero-length-slider")]
|
||||||
|
[TestCase("mania-specific-spinner")]
|
||||||
[TestCase("20544")]
|
[TestCase("20544")]
|
||||||
[TestCase("100374")]
|
[TestCase("100374")]
|
||||||
[TestCase("1450162")]
|
[TestCase("1450162")]
|
||||||
|
[TestCase("4869637")]
|
||||||
public void Test(string name) => base.Test(name);
|
public void Test(string name) => base.Test(name);
|
||||||
|
|
||||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||||
|
File diff suppressed because one or more lines are too long
1442
osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637.osu
Normal file
1442
osu.Game.Rulesets.Mania.Tests/Resources/Testing/Beatmaps/4869637.osu
Normal file
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:
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.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>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -7,11 +7,13 @@ using System.Linq;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Legacy;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
|
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
|
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Objects.Legacy;
|
||||||
using osu.Game.Rulesets.Scoring.Legacy;
|
using osu.Game.Rulesets.Scoring.Legacy;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -124,16 +126,109 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
|
|
||||||
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (original is ManiaHitObject maniaOriginal)
|
LegacyHitObjectType legacyType;
|
||||||
{
|
|
||||||
yield return maniaOriginal;
|
|
||||||
|
|
||||||
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);
|
double startTime = original.StartTime;
|
||||||
foreach (ManiaHitObject obj in objects)
|
double endTime = (original as IHasDuration)?.EndTime ?? startTime;
|
||||||
yield return obj;
|
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);
|
private readonly LimitedCapacityQueue<double> prevNoteTimes = new LimitedCapacityQueue<double>(max_notes_for_density);
|
||||||
@ -156,135 +251,5 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
lastTime = time;
|
lastTime = time;
|
||||||
lastPosition = position;
|
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
|
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; }
|
public PatternType StairType { get; private set; }
|
||||||
|
|
||||||
private readonly PatternType convertType;
|
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)
|
double density, PatternType lastStair)
|
||||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
: 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 (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
|
||||||
// If we convert to 7K + 1, let's not overload the special key
|
// If we convert to 7K + 1, let's not overload the special key
|
||||||
&& (TotalColumns != 8 || lastColumn != 0)
|
&& (TotalColumns != 8 || lastColumn != 0)
|
||||||
// Make sure the last column was not the centre column
|
// Make sure the last column was not the centre column
|
||||||
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
|
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
|
||||||
{
|
{
|
||||||
// Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
|
// Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
|
||||||
int column = RandomStart + TotalColumns - lastColumn - 1;
|
int column = RandomStart + TotalColumns - lastColumn - 1;
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
@ -15,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A pattern generator for legacy hit objects.
|
/// A pattern generator for legacy hit objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class PatternGenerator : Patterns.PatternGenerator
|
internal abstract class LegacyPatternGenerator : PatternGenerator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The column index at which to start generating random notes.
|
/// The column index at which to start generating random notes.
|
||||||
@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly LegacyRandom Random;
|
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)
|
: base(hitObject, beatmap, totalColumns, previousPattern)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(random);
|
ArgumentNullException.ThrowIfNull(random);
|
||||||
@ -96,8 +94,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
if (conversionDifficulty != null)
|
if (conversionDifficulty != null)
|
||||||
return conversionDifficulty.Value;
|
return conversionDifficulty.Value;
|
||||||
|
|
||||||
HitObject lastObject = Beatmap.HitObjects.LastOrDefault();
|
HitObject? lastObject = Beatmap.HitObjects.LastOrDefault();
|
||||||
HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
|
HitObject? firstObject = Beatmap.HitObjects.FirstOrDefault();
|
||||||
|
|
||||||
// Drain time in seconds
|
// Drain time in seconds
|
||||||
int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000);
|
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="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="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="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.
|
/// <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>
|
/// 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
|
/// <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>
|
/// <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>
|
/// <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)
|
params Pattern[] patterns)
|
||||||
{
|
{
|
||||||
lowerBound ??= RandomStart;
|
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"/>).
|
/// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</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"/> 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);
|
protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@ -19,9 +17,9 @@ using osu.Game.Utils;
|
|||||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A pattern generator for IHasDistance hit objects.
|
/// Converter for legacy "Slider" hit objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class PathObjectPatternGenerator : PatternGenerator
|
internal class SliderPatternGenerator : LegacyPatternGenerator
|
||||||
{
|
{
|
||||||
public readonly int StartTime;
|
public readonly int StartTime;
|
||||||
public readonly int EndTime;
|
public readonly int EndTime;
|
||||||
@ -30,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
private PatternType convertType;
|
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)
|
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||||
{
|
{
|
||||||
convertType = PatternType.None;
|
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"/>.
|
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve node samples at.</param>
|
/// <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;
|
return null;
|
||||||
|
|
||||||
int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
@ -12,12 +12,15 @@ using osu.Game.Utils;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
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 int endTime;
|
||||||
private readonly PatternType convertType;
|
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)
|
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||||
{
|
{
|
||||||
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
|
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
|
@ -1,9 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
|
||||||
@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class Pattern
|
internal class Pattern
|
||||||
{
|
{
|
||||||
private List<ManiaHitObject> hitObjects;
|
private List<ManiaHitObject>? hitObjects;
|
||||||
private HashSet<int> containedColumns;
|
private HashSet<int>? containedColumns;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// All the hit objects contained in this pattern.
|
/// All the hit objects contained in this pattern.
|
||||||
@ -72,6 +71,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
|||||||
containedColumns?.Clear();
|
containedColumns?.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(hitObjects), nameof(containedColumns))]
|
||||||
private void prepareStorage()
|
private void prepareStorage()
|
||||||
{
|
{
|
||||||
hitObjects ??= new List<ManiaHitObject>();
|
hitObjects ??= new List<ManiaHitObject>();
|
||||||
|
@ -64,11 +64,11 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
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++)
|
for (int i = 0; i < objectDescriptions.Length; i++)
|
||||||
{
|
{
|
||||||
string[] split = objectDescriptions[i].Split('|').ToArray();
|
string[] split = objectDescriptions[i].Split('|');
|
||||||
if (split.Length != 2)
|
if (split.Length != 2)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
|
|||||||
|
|
||||||
#region LegacyBeatmapEncoder
|
#region LegacyBeatmapEncoder
|
||||||
|
|
||||||
float IHasXPosition.X => Column;
|
float IHasXPosition.X
|
||||||
|
{
|
||||||
|
get => Column;
|
||||||
|
set => Column = (int)value;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
15
osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs
Normal file
15
osu.Game.Rulesets.Osu.Tests.iOS/AppDelegate.cs
Normal file
@ -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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.iOS;
|
using UIKit;
|
||||||
using osu.Game.Tests;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests.iOS
|
namespace osu.Game.Rulesets.Osu.Tests.iOS
|
||||||
{
|
{
|
||||||
public static class Application
|
public static class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
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.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Objects;
|
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.HitCircles.Components;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
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));
|
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
|
private ComposeBlueprintContainer blueprintContainer
|
||||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||||
|
|
||||||
|
100
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs
Normal file
100
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRelax.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
@ -17,6 +18,7 @@ using osu.Framework.Graphics.Textures;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Testing.Input;
|
using osu.Framework.Testing.Input;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -103,6 +105,23 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
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", () =>
|
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
@ -121,12 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private readonly IRenderer renderer;
|
private readonly IRenderer renderer;
|
||||||
private readonly bool provideMiddle;
|
private readonly bool provideMiddle;
|
||||||
private readonly bool provideCursor;
|
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.renderer = renderer;
|
||||||
this.provideMiddle = provideMiddle;
|
this.provideMiddle = provideMiddle;
|
||||||
this.provideCursor = provideCursor;
|
this.provideCursor = provideCursor;
|
||||||
|
this.enableRotation = enableRotation;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
@ -152,7 +173,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
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;
|
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));
|
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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="Moq" Version="4.18.4" />
|
<PackageReference Include="Moq" Version="4.18.4" />
|
||||||
<PackageReference Include="NUnit" Version="3.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>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -137,11 +137,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns>Whether any change actually took place.</returns>
|
||||||
public bool DeleteSelected()
|
public bool DeleteSelected()
|
||||||
{
|
{
|
||||||
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
|
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
|
// Ensure that there are any points to be deleted
|
||||||
if (toRemove.Count == 0)
|
if (toRemove.Count == 0)
|
||||||
return false;
|
return false;
|
||||||
@ -149,11 +165,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
RemoveControlPointsRequested?.Invoke(toRemove);
|
RemoveControlPointsRequested?.Invoke(toRemove);
|
||||||
changeHandler?.EndChange();
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Utils;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||||
{
|
{
|
||||||
@ -76,6 +77,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
base.OnDragEnd(e);
|
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()
|
private void updateState()
|
||||||
{
|
{
|
||||||
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;
|
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;
|
||||||
|
@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
if (hoveredControlPoint == null)
|
if (hoveredControlPoint == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
hoveredControlPoint.IsSelected.Value = true;
|
if (hoveredControlPoint.IsSelected.Value)
|
||||||
ControlPointVisualiser?.DeleteSelected();
|
ControlPointVisualiser?.DeleteSelected();
|
||||||
|
else
|
||||||
|
ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||||
{
|
{
|
||||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
|
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (ControlPointVisualiser == null)
|
if (ControlPointVisualiser == null)
|
||||||
|
@ -53,9 +53,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
|
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
|
||||||
|
|
||||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
protected override IEnumerable<Drawable> CreateTernaryButtons()
|
||||||
=> base.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());
|
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||||
|
|
||||||
private BindableList<HitObject> selectedHitObjects;
|
private BindableList<HitObject> selectedHitObjects;
|
||||||
@ -173,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
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++)
|
for (int i = 0; i < splitDescription.Length; i++)
|
||||||
{
|
{
|
||||||
|
186
osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs
Normal file
186
osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
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.Screens.Edit;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class PreciseMovementPopover : OsuPopover
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
private readonly Dictionary<HitObject, Vector2> initialPositions = new Dictionary<HitObject, Vector2>();
|
||||||
|
private RectangleF initialSurroundingQuad;
|
||||||
|
|
||||||
|
private BindableNumber<float> xBindable = null!;
|
||||||
|
private BindableNumber<float> yBindable = null!;
|
||||||
|
|
||||||
|
private SliderWithTextBoxInput<float> xInput = null!;
|
||||||
|
private OsuCheckbox relativeCheckbox = null!;
|
||||||
|
|
||||||
|
public PreciseMovementPopover()
|
||||||
|
{
|
||||||
|
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Width = 220,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(20),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
xInput = new SliderWithTextBoxInput<float>("X:")
|
||||||
|
{
|
||||||
|
Current = xBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
Precision = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
new SliderWithTextBoxInput<float>("Y:")
|
||||||
|
{
|
||||||
|
Current = yBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
Precision = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
relativeCheckbox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "Relative movement",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ScheduleAfterChildren(() => xInput.TakeFocus());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
editorBeatmap.BeginChange();
|
||||||
|
initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair<HitObject, Vector2>(ho, ((IHasPosition)ho).Position)));
|
||||||
|
initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast<IHasPosition>()).AABBFloat;
|
||||||
|
|
||||||
|
Debug.Assert(initialPositions.Count > 0);
|
||||||
|
|
||||||
|
if (initialPositions.Count > 1)
|
||||||
|
{
|
||||||
|
relativeCheckbox.Current.Value = true;
|
||||||
|
relativeCheckbox.Current.Disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeCheckbox.Current.BindValueChanged(_ => relativeChanged(), true);
|
||||||
|
xBindable.BindValueChanged(_ => applyPosition());
|
||||||
|
yBindable.BindValueChanged(_ => applyPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
if (IsLoaded) editorBeatmap.EndChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void relativeChanged()
|
||||||
|
{
|
||||||
|
// reset bindable bounds to something that is guaranteed to be larger than any previous value.
|
||||||
|
// this prevents crashes that can happen in the middle of changing the bounds, as updating both bound ends at the same is not atomic -
|
||||||
|
// if the old and new bounds are disjoint, assigning X first can produce a situation where MinValue > MaxValue.
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (float.MinValue, float.MaxValue);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (float.MinValue, float.MaxValue);
|
||||||
|
|
||||||
|
float previousX = xBindable.Value;
|
||||||
|
float previousY = yBindable.Value;
|
||||||
|
|
||||||
|
if (relativeCheckbox.Current.Value)
|
||||||
|
{
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y);
|
||||||
|
|
||||||
|
xBindable.Default = yBindable.Default = 0;
|
||||||
|
|
||||||
|
if (initialPositions.Count == 1)
|
||||||
|
{
|
||||||
|
var initialPosition = initialPositions.Single().Value;
|
||||||
|
xBindable.Value = previousX - initialPosition.X;
|
||||||
|
yBindable.Value = previousY - initialPosition.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Assert(initialPositions.Count == 1);
|
||||||
|
var initialPosition = initialPositions.Single().Value;
|
||||||
|
|
||||||
|
var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size);
|
||||||
|
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y);
|
||||||
|
|
||||||
|
xBindable.Default = initialPosition.X;
|
||||||
|
yBindable.Default = initialPosition.Y;
|
||||||
|
|
||||||
|
xBindable.Value = xBindable.Default + previousX;
|
||||||
|
yBindable.Value = yBindable.Default + previousY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPosition()
|
||||||
|
{
|
||||||
|
editorBeatmap.PerformOnSelection(ho =>
|
||||||
|
{
|
||||||
|
if (!initialPositions.TryGetValue(ho, out var initialPosition))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pos = new Vector2(xBindable.Value, yBindable.Value);
|
||||||
|
if (relativeCheckbox.Current.Value)
|
||||||
|
((IHasPosition)ho).Position = initialPosition + pos;
|
||||||
|
else
|
||||||
|
((IHasPosition)ho).Position = pos;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == GlobalAction.Select && !e.Repeat)
|
||||||
|
{
|
||||||
|
this.HidePopover();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnPressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -96,11 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ScheduleAfterChildren(() =>
|
ScheduleAfterChildren(() => angleInput.TakeFocus());
|
||||||
{
|
|
||||||
angleInput.TakeFocus();
|
|
||||||
angleInput.SelectAll();
|
|
||||||
});
|
|
||||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||||
|
|
||||||
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>
|
||||||
|
@ -139,11 +139,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ScheduleAfterChildren(() =>
|
ScheduleAfterChildren(() => scaleInput.TakeFocus());
|
||||||
{
|
|
||||||
scaleInput.TakeFocus();
|
|
||||||
scaleInput.SelectAll();
|
|
||||||
});
|
|
||||||
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
|
||||||
|
|
||||||
xCheckBox.Current.BindValueChanged(_ =>
|
xCheckBox.Current.BindValueChanged(_ =>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -10,6 +11,9 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -18,9 +22,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
|
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
|
||||||
|
private readonly BindableBool canMove = new BindableBool();
|
||||||
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
|
||||||
|
private EditorToolButton moveButton = null!;
|
||||||
private EditorToolButton rotateButton = null!;
|
private EditorToolButton rotateButton = null!;
|
||||||
private EditorToolButton scaleButton = null!;
|
private EditorToolButton scaleButton = null!;
|
||||||
|
|
||||||
@ -35,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(EditorBeatmap editorBeatmap)
|
||||||
{
|
{
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
@ -44,20 +51,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
Spacing = new Vector2(5),
|
Spacing = new Vector2(5),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
moveButton = new EditorToolButton("Move",
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||||
|
() => new PreciseMovementPopover()),
|
||||||
rotateButton = new EditorToolButton("Rotate",
|
rotateButton = new EditorToolButton("Rotate",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||||
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
||||||
scaleButton = new EditorToolButton("Scale",
|
scaleButton = new EditorToolButton("Scale",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ExpandArrowsAlt },
|
||||||
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
selectedHitObjects.BindCollectionChanged((_, _) => canMove.Value = selectedHitObjects.Any(ho => ho is not Spinner), true);
|
||||||
|
|
||||||
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
||||||
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
||||||
|
|
||||||
@ -67,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||||
|
canMove.BindValueChanged(move => moveButton.Enabled.Value = move.NewValue, true);
|
||||||
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
||||||
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
||||||
}
|
}
|
||||||
@ -77,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
|
case GlobalAction.EditorToggleMoveControl:
|
||||||
|
{
|
||||||
|
moveButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
case GlobalAction.EditorToggleRotateControl:
|
case GlobalAction.EditorToggleRotateControl:
|
||||||
{
|
{
|
||||||
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
|
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// How early before a hitobject's start time to trigger a hit.
|
/// How early before a hitobject's start time to trigger a hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const float relax_leniency = 3;
|
public const float RELAX_LENIENCY = 12;
|
||||||
|
|
||||||
private bool isDownState;
|
private bool isDownState;
|
||||||
private bool wasLeft;
|
private bool wasLeft;
|
||||||
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
||||||
{
|
{
|
||||||
// we are not yet close enough to the object.
|
// we are not yet close enough to the object.
|
||||||
if (time < h.HitObject.StartTime - relax_leniency)
|
if (time < h.HitObject.StartTime - RELAX_LENIENCY)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// already hit or beyond the hittable end time.
|
// already hit or beyond the hittable end time.
|
||||||
|
@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
// If samples aren't available at the exact start time of the object,
|
// If samples aren't available at the exact start time of the object,
|
||||||
// use samples (without additions) in the closest original hit object instead
|
// use samples (without additions) in the closest original hit object instead
|
||||||
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList();
|
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.ALL_ADDITIONS.Contains(s.Name)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,13 +377,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
UpdateState(ArmedState.Idle);
|
UpdateState(ArmedState.Idle);
|
||||||
HeadCircle.SuppressHitAnimations();
|
HeadCircle.SuppressHitAnimations();
|
||||||
|
|
||||||
|
foreach (var repeat in repeatContainer)
|
||||||
|
repeat.SuppressHitAnimations();
|
||||||
|
|
||||||
TailCircle.SuppressHitAnimations();
|
TailCircle.SuppressHitAnimations();
|
||||||
|
|
||||||
|
// This method is called every frame in editor contexts, thus the lack of need for transforms.
|
||||||
|
|
||||||
|
if (Time.Current >= HitStateUpdateTime)
|
||||||
|
{
|
||||||
|
// Apply the slider's alpha to *only* the body.
|
||||||
|
// This allows start and – more importantly – end circles to fade slower than the overall slider.
|
||||||
|
if (Alpha < 1)
|
||||||
|
Body.Alpha = Alpha;
|
||||||
|
Alpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LifetimeEnd = HitStateUpdateTime + 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void RestoreHitAnimations()
|
internal void RestoreHitAnimations()
|
||||||
{
|
{
|
||||||
UpdateState(ArmedState.Hit);
|
UpdateState(ArmedState.Hit);
|
||||||
HeadCircle.RestoreHitAnimations();
|
HeadCircle.RestoreHitAnimations();
|
||||||
|
|
||||||
|
foreach (var repeat in repeatContainer)
|
||||||
|
repeat.RestoreHitAnimations();
|
||||||
|
|
||||||
TailCircle.RestoreHitAnimations();
|
TailCircle.RestoreHitAnimations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
|||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -163,5 +164,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
|
||||||
|
|
||||||
|
internal void SuppressHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Idle);
|
||||||
|
UpdateComboColour();
|
||||||
|
|
||||||
|
// This method is called every frame in editor contexts, thus the lack of need for transforms.
|
||||||
|
|
||||||
|
bool hit = Time.Current >= HitStateUpdateTime;
|
||||||
|
|
||||||
|
if (hit)
|
||||||
|
{
|
||||||
|
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
|
||||||
|
AccentColour.Value = Color4.White;
|
||||||
|
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrow.Alpha = hit ? 0 : 1;
|
||||||
|
|
||||||
|
LifetimeEnd = HitStateUpdateTime + 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RestoreHitAnimations()
|
||||||
|
{
|
||||||
|
UpdateState(ArmedState.Hit);
|
||||||
|
UpdateComboColour();
|
||||||
|
Arrow.Alpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
set => position.Value = value;
|
set => position.Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float X => Position.X;
|
public float X
|
||||||
public float Y => Position.Y;
|
{
|
||||||
|
get => Position.X;
|
||||||
|
set => Position = new Vector2(value, Position.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Y
|
||||||
|
{
|
||||||
|
get => Position.Y;
|
||||||
|
set => Position = new Vector2(Position.X, value);
|
||||||
|
}
|
||||||
|
|
||||||
public Vector2 StackedPosition => Position + StackOffset;
|
public Vector2 StackedPosition => Position + StackOffset;
|
||||||
|
|
||||||
@ -175,27 +184,26 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
// Note that this implementation is shared with the osu!catch ruleset's implementation.
|
// Note that this implementation is shared with the osu!catch ruleset's implementation.
|
||||||
// If a change is made here, CatchHitObject.cs should also be updated.
|
// If a change is made here, CatchHitObject.cs should also be updated.
|
||||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
int index = lastObj?.ComboIndex ?? 0;
|
||||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||||
|
|
||||||
if (this is Spinner)
|
// - 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 spinner 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 Spinner && (NewCombo || lastObj == null || lastObj is Spinner))
|
||||||
{
|
{
|
||||||
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
inCurrentCombo = 0;
|
||||||
return;
|
index++;
|
||||||
}
|
indexWithOffsets += ComboOffset + 1;
|
||||||
|
|
||||||
// At decode time, the first hitobject in the beatmap and the first hitobject after a spinner 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 Spinner)
|
|
||||||
{
|
|
||||||
IndexInCurrentCombo = 0;
|
|
||||||
ComboIndex++;
|
|
||||||
ComboIndexWithOffsets += ComboOffset + 1;
|
|
||||||
|
|
||||||
if (lastObj != null)
|
if (lastObj != null)
|
||||||
lastObj.LastInCombo = true;
|
lastObj.LastInCombo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComboIndex = index;
|
||||||
|
ComboIndexWithOffsets = indexWithOffsets;
|
||||||
|
IndexInCurrentCombo = inCurrentCombo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
||||||
|
@ -5,12 +5,12 @@ using System;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
@ -75,44 +75,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
|
|
||||||
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
||||||
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
|
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
|
||||||
|
|
||||||
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
protected override void Update()
|
||||||
{
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||||
|
{
|
||||||
|
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||||
|
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
Scale = Vector2.One;
|
||||||
|
|
||||||
const float move_distance = -12;
|
const float move_distance = -12;
|
||||||
|
const float scale_amount = 1.3f;
|
||||||
|
|
||||||
const double move_out_duration = 35;
|
const double move_out_duration = 35;
|
||||||
const double move_in_duration = 250;
|
const double move_in_duration = 250;
|
||||||
const double total = 300;
|
const double total = 300;
|
||||||
|
|
||||||
switch (state)
|
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
|
||||||
{
|
|
||||||
case ArmedState.Idle:
|
|
||||||
main.ScaleTo(1.3f, move_out_duration, Easing.Out)
|
|
||||||
.Then()
|
|
||||||
.ScaleTo(1f, move_in_duration, Easing.Out)
|
|
||||||
.Loop(total - (move_in_duration + move_out_duration));
|
|
||||||
side
|
|
||||||
.MoveToX(move_distance, move_out_duration, Easing.Out)
|
|
||||||
.Then()
|
|
||||||
.MoveToX(0, move_in_duration, Easing.Out)
|
|
||||||
.Loop(total - (move_in_duration + move_out_duration));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ArmedState.Hit:
|
if (loopCurrentTime < move_out_duration)
|
||||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
|
||||||
this.ScaleTo(1.5f, animDuration, Easing.Out);
|
else
|
||||||
break;
|
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
if (loopCurrentTime < move_out_duration)
|
||||||
{
|
side.X = Interpolation.ValueAt(loopCurrentTime, 0, move_distance, 0, move_out_duration, Easing.Out);
|
||||||
base.Dispose(isDisposing);
|
else
|
||||||
|
side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 0, move_out_duration, move_out_duration + move_in_duration, Easing.Out);
|
||||||
if (drawableRepeat.IsNotNull())
|
|
||||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
@ -40,37 +40,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
private void load(DrawableHitObject drawableObject)
|
private void load(DrawableHitObject drawableObject)
|
||||||
{
|
{
|
||||||
drawableRepeat = (DrawableSliderRepeat)drawableObject;
|
drawableRepeat = (DrawableSliderRepeat)drawableObject;
|
||||||
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
protected override void Update()
|
||||||
{
|
{
|
||||||
const double move_out_duration = 35;
|
base.Update();
|
||||||
const double move_in_duration = 250;
|
|
||||||
const double total = 300;
|
|
||||||
|
|
||||||
switch (state)
|
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||||
{
|
{
|
||||||
case ArmedState.Idle:
|
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||||
InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out)
|
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||||
.Then()
|
|
||||||
.ScaleTo(1f, move_in_duration, Easing.Out)
|
|
||||||
.Loop(total - (move_in_duration + move_out_duration));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ArmedState.Hit:
|
|
||||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
|
||||||
InternalChild.ScaleTo(1.5f, animDuration, Easing.Out);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
|
const float scale_amount = 1.3f;
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
const double move_out_duration = 35;
|
||||||
{
|
const double move_in_duration = 250;
|
||||||
base.Dispose(isDisposing);
|
const double total = 300;
|
||||||
|
|
||||||
if (drawableRepeat.IsNotNull())
|
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
|
||||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
if (loopCurrentTime < move_out_duration)
|
||||||
|
Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
|
||||||
|
else
|
||||||
|
Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class LegacyCursor : SkinnableCursor
|
public partial class LegacyCursor : SkinnableCursor
|
||||||
{
|
{
|
||||||
|
public static readonly int REVOLUTION_DURATION = 10000;
|
||||||
|
|
||||||
private const float pressed_scale = 1.3f;
|
private const float pressed_scale = 1.3f;
|
||||||
private const float released_scale = 1f;
|
private const float released_scale = 1f;
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
if (spin)
|
if (spin)
|
||||||
ExpandTarget.Spin(10000, RotationDirection.Clockwise);
|
ExpandTarget.Spin(REVOLUTION_DURATION, RotationDirection.Clockwise);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Expand()
|
public override void Expand()
|
||||||
|
@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
private void load(OsuConfigManager config, ISkinSource skinSource)
|
private void load(OsuConfigManager config, ISkinSource skinSource)
|
||||||
{
|
{
|
||||||
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
||||||
|
AllowPartRotation = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true;
|
||||||
|
|
||||||
Texture = skin.GetTexture("cursortrail");
|
Texture = skin.GetTexture("cursortrail");
|
||||||
|
|
||||||
|
@ -61,13 +61,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
|
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
|
||||||
|
|
||||||
// As a precondition, ensure that any prefix lookups are run against the skin which is providing "hitcircle".
|
// As a precondition, prefer that any *prefix* lookups are run against the skin which is providing "hitcircle".
|
||||||
// This is to correctly handle a case such as:
|
// This is to correctly handle a case such as:
|
||||||
//
|
//
|
||||||
// - Beatmap provides `hitcircle`
|
// - Beatmap provides `hitcircle`
|
||||||
// - User skin provides `sliderstartcircle`
|
// - User skin provides `sliderstartcircle`
|
||||||
//
|
//
|
||||||
// In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override.
|
// In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override.
|
||||||
|
//
|
||||||
|
// Of note, this consideration should only be used to decide whether to continue looking up the prefixed name or not.
|
||||||
|
// The final lookups must still run on the full skin hierarchy as per usual in order to correctly handle fallback cases.
|
||||||
var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin;
|
var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin;
|
||||||
|
|
||||||
// if a base texture for the specified prefix exists, continue using it for subsequent lookups.
|
// if a base texture for the specified prefix exists, continue using it for subsequent lookups.
|
||||||
@ -81,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(circleName)?.WithMaximumSize(maxSize) })
|
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) })
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
|
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -9,10 +9,12 @@ using osu.Framework.Extensions.ObjectExtensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
@ -51,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
||||||
|
|
||||||
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
|
||||||
|
|
||||||
shouldRotate = skinSource.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value <= 1;
|
shouldRotate = skinSource.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
||||||
accentColour.BindValueChanged(c =>
|
accentColour.BindValueChanged(c =>
|
||||||
{
|
{
|
||||||
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
|
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > 600 / 255f ? Color4.Black : Color4.White;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,36 +80,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
|
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
protected override void Update()
|
||||||
{
|
{
|
||||||
const double duration = 300;
|
base.Update();
|
||||||
const float rotation = 5.625f;
|
|
||||||
|
|
||||||
switch (state)
|
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||||
{
|
{
|
||||||
case ArmedState.Idle:
|
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||||
if (shouldRotate)
|
arrow.Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.4f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||||
{
|
}
|
||||||
InternalChild.ScaleTo(1.3f)
|
else
|
||||||
.RotateTo(rotation)
|
{
|
||||||
.Then()
|
const double duration = 300;
|
||||||
.ScaleTo(1f, duration)
|
const float rotation = 5.625f;
|
||||||
.RotateTo(-rotation, duration)
|
|
||||||
.Loop();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
InternalChild.ScaleTo(1.3f).Then()
|
|
||||||
.ScaleTo(1f, duration, Easing.Out)
|
|
||||||
.Loop();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration;
|
||||||
|
|
||||||
case ArmedState.Hit:
|
// Reference: https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameplayElements/HitObjects/Osu/HitCircleSliderEnd.cs#L79-L96
|
||||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
if (shouldRotate)
|
||||||
InternalChild.ScaleTo(1.4f, animDuration, Easing.Out);
|
{
|
||||||
break;
|
arrow.Rotation = Interpolation.ValueAt(loopCurrentTime, rotation, -rotation, 0, duration);
|
||||||
|
arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration, Easing.Out));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +116,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
if (drawableRepeat.IsNotNull())
|
if (drawableRepeat.IsNotNull())
|
||||||
{
|
{
|
||||||
drawableRepeat.HitObjectApplied -= onHitObjectApplied;
|
drawableRepeat.HitObjectApplied -= onHitObjectApplied;
|
||||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
CursorCentre,
|
CursorCentre,
|
||||||
CursorExpand,
|
CursorExpand,
|
||||||
CursorRotate,
|
CursorRotate,
|
||||||
|
CursorTrailRotate,
|
||||||
HitCircleOverlayAboveNumber,
|
HitCircleOverlayAboveNumber,
|
||||||
|
|
||||||
// ReSharper disable once IdentifierTypo
|
// ReSharper disable once IdentifierTypo
|
||||||
|
@ -34,19 +34,24 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual float FadeExponent => 1.7f;
|
protected virtual float FadeExponent => 1.7f;
|
||||||
|
|
||||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
|
||||||
private int currentIndex;
|
|
||||||
private IShader shader;
|
|
||||||
private double timeOffset;
|
|
||||||
private float time;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The scale used on creation of a new trail part.
|
/// The scale used on creation of a new trail part.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector2 NewPartScale = Vector2.One;
|
public Vector2 NewPartScale { get; set; } = Vector2.One;
|
||||||
|
|
||||||
private Anchor trailOrigin = Anchor.Centre;
|
/// <summary>
|
||||||
|
/// The rotation (in degrees) to apply to trail parts when <see cref="AllowPartRotation"/> is <c>true</c>.
|
||||||
|
/// </summary>
|
||||||
|
public float PartRotation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to rotate trail parts based on the value of <see cref="PartRotation"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected bool AllowPartRotation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The trail part texture origin.
|
||||||
|
/// </summary>
|
||||||
protected Anchor TrailOrigin
|
protected Anchor TrailOrigin
|
||||||
{
|
{
|
||||||
get => trailOrigin;
|
get => trailOrigin;
|
||||||
@ -57,6 +62,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||||
|
private Anchor trailOrigin = Anchor.Centre;
|
||||||
|
private int currentIndex;
|
||||||
|
private IShader shader;
|
||||||
|
private double timeOffset;
|
||||||
|
private float time;
|
||||||
|
|
||||||
public CursorTrail()
|
public CursorTrail()
|
||||||
{
|
{
|
||||||
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
|
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
|
||||||
@ -220,6 +232,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
private float time;
|
private float time;
|
||||||
private float fadeExponent;
|
private float fadeExponent;
|
||||||
|
private float angle;
|
||||||
|
|
||||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||||
private Vector2 originPosition;
|
private Vector2 originPosition;
|
||||||
@ -239,6 +252,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
texture = Source.texture;
|
texture = Source.texture;
|
||||||
time = Source.time;
|
time = Source.time;
|
||||||
fadeExponent = Source.FadeExponent;
|
fadeExponent = Source.FadeExponent;
|
||||||
|
angle = Source.AllowPartRotation ? float.DegreesToRadians(Source.PartRotation) : 0;
|
||||||
|
|
||||||
originPosition = Vector2.Zero;
|
originPosition = Vector2.Zero;
|
||||||
|
|
||||||
@ -279,6 +293,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
renderer.PushLocalMatrix(DrawInfo.Matrix);
|
renderer.PushLocalMatrix(DrawInfo.Matrix);
|
||||||
|
|
||||||
|
float sin = MathF.Sin(angle);
|
||||||
|
float cos = MathF.Cos(angle);
|
||||||
|
|
||||||
foreach (var part in parts)
|
foreach (var part in parts)
|
||||||
{
|
{
|
||||||
if (part.InvalidationID == -1)
|
if (part.InvalidationID == -1)
|
||||||
@ -289,7 +306,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
Position = rotateAround(
|
||||||
|
new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||||
|
part.Position, sin, cos),
|
||||||
TexturePosition = textureRect.BottomLeft,
|
TexturePosition = textureRect.BottomLeft,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
||||||
@ -298,7 +317,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
Position = rotateAround(
|
||||||
|
new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X,
|
||||||
|
part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos),
|
||||||
TexturePosition = textureRect.BottomRight,
|
TexturePosition = textureRect.BottomRight,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
||||||
@ -307,7 +328,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
Position = rotateAround(
|
||||||
|
new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||||
|
part.Position, sin, cos),
|
||||||
TexturePosition = textureRect.TopRight,
|
TexturePosition = textureRect.TopRight,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
||||||
@ -316,7 +339,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
Position = rotateAround(
|
||||||
|
new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||||
|
part.Position, sin, cos),
|
||||||
TexturePosition = textureRect.TopLeft,
|
TexturePosition = textureRect.TopLeft,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
||||||
@ -330,6 +355,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
shader.Unbind();
|
shader.Unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Vector2 rotateAround(Vector2 input, Vector2 origin, float sin, float cos)
|
||||||
|
{
|
||||||
|
float xTranslated = input.X - origin.X;
|
||||||
|
float yTranslated = input.Y - origin.Y;
|
||||||
|
|
||||||
|
return new Vector2(xTranslated * cos - yTranslated * sin, xTranslated * sin + yTranslated * cos) + origin;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -36,6 +36,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
|
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current rotation of the cursor.
|
||||||
|
/// </summary>
|
||||||
|
public float CurrentRotation => skinnableCursor.ExpandTarget?.Rotation ?? 0;
|
||||||
|
|
||||||
public IBindable<float> CursorScale => cursorScale;
|
public IBindable<float> CursorScale => cursorScale;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -83,7 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (cursorTrail.Drawable is CursorTrail trail)
|
if (cursorTrail.Drawable is CursorTrail trail)
|
||||||
|
{
|
||||||
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
|
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
|
||||||
|
trail.PartRotation = ActiveCursor.CurrentRotation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||||
|
15
osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs
Normal file
15
osu.Game.Rulesets.Taiko.Tests.iOS/AppDelegate.cs
Normal file
@ -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.Taiko.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.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.iOS;
|
using UIKit;
|
||||||
using osu.Game.Tests;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Tests.iOS
|
namespace osu.Game.Rulesets.Taiko.Tests.iOS
|
||||||
{
|
{
|
||||||
public static class Application
|
public static class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
GameApplication.Main(new OsuTestBrowser());
|
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.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>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
14
osu.Game.Tests.iOS/AppDelegate.cs
Normal file
14
osu.Game.Tests.iOS/AppDelegate.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.iOS
|
||||||
|
{
|
||||||
|
[Register("AppDelegate")]
|
||||||
|
public class AppDelegate : GameApplicationDelegate
|
||||||
|
{
|
||||||
|
protected override Framework.Game CreateGame() => new OsuTestBrowser();
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,15 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.iOS;
|
using UIKit;
|
||||||
|
|
||||||
namespace osu.Game.Tests.iOS
|
namespace osu.Game.Tests.iOS
|
||||||
{
|
{
|
||||||
public static class Application
|
public static class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
GameApplication.Main(new OsuTestBrowser());
|
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO.Archives;
|
using osu.Game.IO.Archives;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
using MemoryStream = System.IO.MemoryStream;
|
using MemoryStream = System.IO.MemoryStream;
|
||||||
@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO
|
|||||||
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
|
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestFractionalObjectCoordinatesRounded()
|
||||||
|
{
|
||||||
|
IWorkingBeatmap beatmap = null!;
|
||||||
|
MemoryStream outStream = null!;
|
||||||
|
|
||||||
|
// Ensure importer encoding is correct
|
||||||
|
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz"));
|
||||||
|
AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001));
|
||||||
|
|
||||||
|
// Ensure exporter legacy conversion is correct
|
||||||
|
AddStep("export", () =>
|
||||||
|
{
|
||||||
|
outStream = new MemoryStream();
|
||||||
|
|
||||||
|
new LegacyBeatmapExporter(LocalStorage)
|
||||||
|
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
|
||||||
|
AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestExportStability()
|
public void TestExportStability()
|
||||||
{
|
{
|
||||||
|
@ -112,5 +112,20 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRepeatsGeneratedEvenForZeroLengthSlider()
|
||||||
|
{
|
||||||
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, 0, 2).ToArray();
|
||||||
|
|
||||||
|
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
||||||
|
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
||||||
|
|
||||||
|
Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Repeat));
|
||||||
|
Assert.That(events[1].Time, Is.EqualTo(span_duration));
|
||||||
|
|
||||||
|
Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tail));
|
||||||
|
Assert.That(events[3].Time, Is.EqualTo(span_duration * 2));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -539,5 +539,85 @@ namespace osu.Game.Tests.Editing
|
|||||||
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
|
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPuttingObjectBetweenBreakEndAndAnotherObjectForcesNewCombo()
|
||||||
|
{
|
||||||
|
var controlPoints = new ControlPointInfo();
|
||||||
|
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
var beatmap = new EditorBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
ControlPointInfo = controlPoints,
|
||||||
|
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
|
||||||
|
Difficulty =
|
||||||
|
{
|
||||||
|
ApproachRate = 10,
|
||||||
|
},
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||||
|
new HitCircle { StartTime = 4500 },
|
||||||
|
new HitCircle { StartTime = 5000, NewCombo = true },
|
||||||
|
},
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(2000, 4000),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var ho in beatmap.HitObjects)
|
||||||
|
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||||
|
|
||||||
|
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
|
||||||
|
beatmapProcessor.PreProcess();
|
||||||
|
beatmapProcessor.PostProcess();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True);
|
||||||
|
Assert.That(((HitCircle)beatmap.HitObjects[2]).NewCombo, Is.True);
|
||||||
|
|
||||||
|
Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1));
|
||||||
|
Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2));
|
||||||
|
Assert.That(((HitCircle)beatmap.HitObjects[2]).ComboIndex, Is.EqualTo(3));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAutomaticallyInsertedBreakForcesNewCombo()
|
||||||
|
{
|
||||||
|
var controlPoints = new ControlPointInfo();
|
||||||
|
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||||
|
var beatmap = new EditorBeatmap(new Beatmap
|
||||||
|
{
|
||||||
|
ControlPointInfo = controlPoints,
|
||||||
|
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
|
||||||
|
Difficulty =
|
||||||
|
{
|
||||||
|
ApproachRate = 10,
|
||||||
|
},
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||||
|
new HitCircle { StartTime = 5000 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var ho in beatmap.HitObjects)
|
||||||
|
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||||
|
|
||||||
|
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
|
||||||
|
beatmapProcessor.PreProcess();
|
||||||
|
beatmapProcessor.PostProcess();
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
|
||||||
|
Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True);
|
||||||
|
|
||||||
|
Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1));
|
||||||
|
Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
osu.Game.Tests/Resources/Archives/fractional-coordinates.olz
Normal file
BIN
osu.Game.Tests/Resources/Archives/fractional-coordinates.olz
Normal file
Binary file not shown.
BIN
osu.Game.Tests/Resources/Archives/modified-default-20241207.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-default-20241207.osk
Normal file
Binary file not shown.
@ -68,7 +68,9 @@ namespace osu.Game.Tests.Skins
|
|||||||
// Covers legacy rank display
|
// Covers legacy rank display
|
||||||
"Archives/modified-classic-20230809.osk",
|
"Archives/modified-classic-20230809.osk",
|
||||||
// Covers legacy key counter
|
// Covers legacy key counter
|
||||||
"Archives/modified-classic-20240724.osk"
|
"Archives/modified-classic-20240724.osk",
|
||||||
|
// Covers skinnable mod display
|
||||||
|
"Archives/modified-default-20241207.osk",
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -131,21 +131,6 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
assertNoBackgrounds();
|
assertNoBackgrounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDelayedConnectivity()
|
|
||||||
{
|
|
||||||
registerBackgroundsResponse(DateTimeOffset.Now.AddDays(30));
|
|
||||||
setSeasonalBackgroundMode(SeasonalBackgroundMode.Always);
|
|
||||||
AddStep("go offline", () => dummyAPI.SetState(APIState.Offline));
|
|
||||||
|
|
||||||
createLoader();
|
|
||||||
assertNoBackgrounds();
|
|
||||||
|
|
||||||
AddStep("go online", () => dummyAPI.SetState(APIState.Online));
|
|
||||||
|
|
||||||
assertAnyBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerBackgroundsResponse(DateTimeOffset endDate)
|
private void registerBackgroundsResponse(DateTimeOffset endDate)
|
||||||
=> AddStep("setup request handler", () =>
|
=> AddStep("setup request handler", () =>
|
||||||
{
|
{
|
||||||
@ -185,7 +170,8 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
{
|
{
|
||||||
previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault();
|
previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault();
|
||||||
background = backgroundLoader.LoadNextBackground();
|
background = backgroundLoader.LoadNextBackground();
|
||||||
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
|
if (background != null)
|
||||||
|
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("background loaded", () => background.IsLoaded);
|
AddUntilStep("background loaded", () => background.IsLoaded);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -15,6 +16,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Input.States;
|
using osu.Framework.Input.States;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -31,6 +33,7 @@ using osu.Game.Screens.Play;
|
|||||||
using osu.Game.Screens.Play.PlayerSettings;
|
using osu.Game.Screens.Play.PlayerSettings;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Storyboards.Drawables;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -45,6 +48,7 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
private LoadBlockingTestPlayer player;
|
private LoadBlockingTestPlayer player;
|
||||||
private BeatmapManager manager;
|
private BeatmapManager manager;
|
||||||
private RulesetStore rulesets;
|
private RulesetStore rulesets;
|
||||||
|
private UpdateCounter storyboardUpdateCounter;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, AudioManager audio)
|
private void load(GameHost host, AudioManager audio)
|
||||||
@ -194,6 +198,28 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStoryboardUpdatesWhenDimmed()
|
||||||
|
{
|
||||||
|
performFullSetup();
|
||||||
|
createFakeStoryboard();
|
||||||
|
|
||||||
|
AddStep("Enable fully dimmed storyboard", () =>
|
||||||
|
{
|
||||||
|
player.StoryboardReplacesBackground.Value = true;
|
||||||
|
player.StoryboardEnabled.Value = true;
|
||||||
|
player.DimmableStoryboard.IgnoreUserSettings.Value = false;
|
||||||
|
songSelect.DimLevel.Value = 1f;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
|
||||||
|
|
||||||
|
AddWaitStep("wait some", 20);
|
||||||
|
|
||||||
|
AddUntilStep("Storyboard is always present", () => player.ChildrenOfType<DrawableStoryboard>().Single().AlwaysPresent, () => Is.True);
|
||||||
|
AddUntilStep("Dimmable storyboard content is being updated", () => storyboardUpdateCounter.StoryboardContentLastUpdated, () => Is.EqualTo(Time.Current).Within(100));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestStoryboardIgnoreUserSettings()
|
public void TestStoryboardIgnoreUserSettings()
|
||||||
{
|
{
|
||||||
@ -269,15 +295,19 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
{
|
{
|
||||||
player.StoryboardEnabled.Value = false;
|
player.StoryboardEnabled.Value = false;
|
||||||
player.StoryboardReplacesBackground.Value = false;
|
player.StoryboardReplacesBackground.Value = false;
|
||||||
player.DimmableStoryboard.Add(new OsuSpriteText
|
player.DimmableStoryboard.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
Size = new Vector2(500, 50),
|
storyboardUpdateCounter = new UpdateCounter(),
|
||||||
Alpha = 1,
|
new OsuSpriteText
|
||||||
Colour = Color4.White,
|
{
|
||||||
Anchor = Anchor.Centre,
|
Size = new Vector2(500, 50),
|
||||||
Origin = Anchor.Centre,
|
Alpha = 1,
|
||||||
Text = "THIS IS A STORYBOARD",
|
Colour = Color4.White,
|
||||||
Font = new FontUsage(size: 50)
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Text = "THIS IS A STORYBOARD",
|
||||||
|
Font = new FontUsage(size: 50)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -353,7 +383,7 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Make sure every time a screen gets pushed, the background doesn't get replaced
|
/// Make sure every time a screen gets pushed, the background doesn't get replaced
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Whether or not the original background (The one created in DummySongSelect) is still the current background</returns>
|
/// <returns>Whether the original background (The one created in DummySongSelect) is still the current background</returns>
|
||||||
public bool IsBackgroundCurrent() => background?.IsCurrentScreen() == true;
|
public bool IsBackgroundCurrent() => background?.IsCurrentScreen() == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +414,7 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
|
|
||||||
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
|
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
|
||||||
|
|
||||||
// Whether or not the player should be allowed to load.
|
// Whether the player should be allowed to load.
|
||||||
public bool BlockLoad;
|
public bool BlockLoad;
|
||||||
|
|
||||||
public Bindable<bool> StoryboardEnabled;
|
public Bindable<bool> StoryboardEnabled;
|
||||||
@ -451,6 +481,17 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class UpdateCounter : Drawable
|
||||||
|
{
|
||||||
|
public double StoryboardContentLastUpdated;
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
StoryboardContentLastUpdated = Time.Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private partial class TestDimmableBackground : BackgroundScreenBeatmap.DimmableBackground
|
private partial class TestDimmableBackground : BackgroundScreenBeatmap.DimmableBackground
|
||||||
{
|
{
|
||||||
public Color4 CurrentColour => Content.Colour;
|
public Color4 CurrentColour => Content.Colour;
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
// 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;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Online;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
using osu.Game.Online.Metadata;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
using osu.Game.Tests.Visual.Metadata;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Components
|
||||||
|
{
|
||||||
|
public partial class TestSceneFriendPresenceNotifier : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
private ChannelManager channelManager = null!;
|
||||||
|
private NotificationOverlay notificationOverlay = null!;
|
||||||
|
private ChatOverlay chatOverlay = null!;
|
||||||
|
private TestMetadataClient metadataClient = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Child = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(ChannelManager), channelManager = new ChannelManager(API)),
|
||||||
|
(typeof(INotificationOverlay), notificationOverlay = new NotificationOverlay()),
|
||||||
|
(typeof(ChatOverlay), chatOverlay = new ChatOverlay()),
|
||||||
|
(typeof(MetadataClient), metadataClient = new TestMetadataClient()),
|
||||||
|
],
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
channelManager,
|
||||||
|
notificationOverlay,
|
||||||
|
chatOverlay,
|
||||||
|
metadataClient,
|
||||||
|
new FriendPresenceNotifier()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 1; i <= 100; i++)
|
||||||
|
((DummyAPIAccess)API).Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNotifications()
|
||||||
|
{
|
||||||
|
AddStep("bring friend 1 online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
AddStep("bring friend 1 offline", () => metadataClient.FriendPresenceUpdated(1, null));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleUserNotificationOpensChat()
|
||||||
|
{
|
||||||
|
AddStep("bring friend 1 online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("click notification", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<Notification>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("chat overlay opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||||
|
AddUntilStep("user channel selected", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo(((DummyAPIAccess)API).Friends[0].TargetUser!.Username));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleUserNotificationDoesNotOpenChat()
|
||||||
|
{
|
||||||
|
AddStep("bring friends 1 & 2 online", () =>
|
||||||
|
{
|
||||||
|
metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online });
|
||||||
|
metadataClient.FriendPresenceUpdated(2, new UserPresence { Status = UserStatus.Online });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("click notification", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<Notification>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("chat overlay not opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNonFriendsDoNotNotify()
|
||||||
|
{
|
||||||
|
AddStep("bring non-friend 1000 online", () => metadataClient.UserPresenceUpdated(1000, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddWaitStep("wait for possible notification", 10);
|
||||||
|
AddAssert("no notification", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPostManyDebounced()
|
||||||
|
{
|
||||||
|
AddStep("bring friends 1-10 online", () =>
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 10; i++)
|
||||||
|
metadataClient.FriendPresenceUpdated(i, new UserPresence { Status = UserStatus.Online });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("bring friends 1-10 offline", () =>
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 10; i++)
|
||||||
|
metadataClient.FriendPresenceUpdated(i, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,18 +7,18 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.Backgrounds;
|
using osu.Game.Screens.Backgrounds;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
||||||
using osu.Game.Screens.Edit.GameplayTest;
|
using osu.Game.Screens.Edit.GameplayTest;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
@ -42,14 +42,6 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
private BeatmapSetInfo importedBeatmapSet;
|
private BeatmapSetInfo importedBeatmapSet;
|
||||||
|
|
||||||
private Bindable<float> editorDim;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuConfigManager config)
|
|
||||||
{
|
|
||||||
editorDim = config.GetBindable<float>(OsuSetting.EditorDim);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetUpSteps()
|
public override void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely());
|
AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely());
|
||||||
@ -80,15 +72,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
|
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
|
||||||
AddStep("exit player", () => editorPlayer.Exit());
|
AddStep("exit player", () => editorPlayer.Exit());
|
||||||
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
|
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
|
||||||
AddUntilStep("background has correct params", () =>
|
AddUntilStep("background is correct", () => this.ChildrenOfType<BackgroundScreenStack>().Single().CurrentScreen is EditorBackgroundScreen);
|
||||||
{
|
|
||||||
// the test gameplay player's beatmap may be the "same" beatmap as the one being edited, *but* the `BeatmapInfo` references may differ
|
|
||||||
// due to the beatmap refetch logic ran on editor suspend.
|
|
||||||
// this test cares about checking the background belonging to the editor specifically, so check that using reference equality
|
|
||||||
// (as `.Equals()` cannot discern between the two, as they technically share the same database GUID).
|
|
||||||
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo));
|
|
||||||
return background.DimWhenUserSettingsIgnored.Value == editorDim.Value && background.BlurAmount.Value == 0;
|
|
||||||
});
|
|
||||||
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
|
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,20 +97,41 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddStep("exit player", () => editorPlayer.Exit());
|
AddStep("exit player", () => editorPlayer.Exit());
|
||||||
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
|
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
|
||||||
AddUntilStep("background has correct params", () =>
|
AddUntilStep("background is correct", () => this.ChildrenOfType<BackgroundScreenStack>().Single().CurrentScreen is EditorBackgroundScreen);
|
||||||
{
|
|
||||||
// the test gameplay player's beatmap may be the "same" beatmap as the one being edited, *but* the `BeatmapInfo` references may differ
|
|
||||||
// due to the beatmap refetch logic ran on editor suspend.
|
|
||||||
// this test cares about checking the background belonging to the editor specifically, so check that using reference equality
|
|
||||||
// (as `.Equals()` cannot discern between the two, as they technically share the same database GUID).
|
|
||||||
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo));
|
|
||||||
return background.DimWhenUserSettingsIgnored.Value == editorDim.Value && background.BlurAmount.Value == 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
AddStep("start track", () => EditorClock.Start());
|
AddStep("start track", () => EditorClock.Start());
|
||||||
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
|
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGameplayTestResetsPlaybackSpeedAdjustment()
|
||||||
|
{
|
||||||
|
AddStep("start track", () => EditorClock.Start());
|
||||||
|
AddStep("change playback speed", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<PlaybackControl.PlaybackTabControl.PlaybackTabItem>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddAssert("track playback rate is 0.25x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
|
||||||
|
|
||||||
|
AddStep("click test gameplay button", () =>
|
||||||
|
{
|
||||||
|
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
|
||||||
|
|
||||||
|
InputManager.MoveMouseTo(button);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
EditorPlayer editorPlayer = null;
|
||||||
|
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
|
||||||
|
AddAssert("editor track stopped", () => !EditorClock.IsRunning);
|
||||||
|
AddAssert("track playback rate is 1x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("exit player", () => editorPlayer.Exit());
|
||||||
|
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
|
||||||
|
AddAssert("track playback rate is 0.25x", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(2000)] // chosen to be after last object in the map
|
[TestCase(2000)] // chosen to be after last object in the map
|
||||||
[TestCase(22000)] // chosen to be in the middle of the last spinner
|
[TestCase(22000)] // chosen to be in the middle of the last spinner
|
||||||
public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd)
|
public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd)
|
||||||
@ -177,6 +182,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
// bit of a hack to ensure this test can be ran multiple times without running into UNIQUE constraint failures
|
// bit of a hack to ensure this test can be ran multiple times without running into UNIQUE constraint failures
|
||||||
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = Guid.NewGuid().ToString());
|
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = Guid.NewGuid().ToString());
|
||||||
|
|
||||||
|
AddStep("start playing track", () => InputManager.Key(Key.Space));
|
||||||
AddStep("click test gameplay button", () =>
|
AddStep("click test gameplay button", () =>
|
||||||
{
|
{
|
||||||
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
|
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
|
||||||
@ -185,11 +191,13 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||||
|
AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning);
|
||||||
|
|
||||||
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||||
|
|
||||||
EditorPlayer editorPlayer = null;
|
EditorPlayer editorPlayer = null;
|
||||||
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
|
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
|
||||||
|
AddUntilStep("track playing", () => Beatmap.Value.Track.IsRunning);
|
||||||
AddAssert("beatmap has 1 object", () => editorPlayer.Beatmap.Value.Beatmap.HitObjects.Count == 1);
|
AddAssert("beatmap has 1 object", () => editorPlayer.Beatmap.Value.Beatmap.HitObjects.Count == 1);
|
||||||
|
|
||||||
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor);
|
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor);
|
||||||
|
@ -27,18 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("Create control", () =>
|
recreateControl();
|
||||||
{
|
|
||||||
Child = new PlayerSettingsGroup("Some settings")
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
offsetControl = new BeatmapOffsetControl()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -123,13 +112,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCalibrationFromZero()
|
public void TestCalibrationFromZero()
|
||||||
{
|
{
|
||||||
|
ScoreInfo referenceScore = null!;
|
||||||
const double average_error = -4.5;
|
const double average_error = -4.5;
|
||||||
|
|
||||||
AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0);
|
AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0);
|
||||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||||
AddStep("Set reference score", () =>
|
AddStep("Set reference score", () =>
|
||||||
{
|
{
|
||||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
offsetControl.ReferenceScore.Value = referenceScore = new ScoreInfo
|
||||||
{
|
{
|
||||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
|
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
|
||||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||||
@ -143,6 +133,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
|
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
|
||||||
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
|
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
|
||||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||||
|
|
||||||
|
recreateControl();
|
||||||
|
AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore);
|
||||||
|
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -251,5 +245,21 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
|
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
|
||||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void recreateControl()
|
||||||
|
{
|
||||||
|
AddStep("Create control", () =>
|
||||||
|
{
|
||||||
|
Child = new PlayerSettingsGroup("Some settings")
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
offsetControl = new BeatmapOffsetControl()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
@ -19,6 +20,7 @@ using osu.Game.Rulesets;
|
|||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Storyboards;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -28,6 +30,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
protected new PausePlayer Player => (PausePlayer)base.Player;
|
protected new PausePlayer Player => (PausePlayer)base.Player;
|
||||||
|
|
||||||
|
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||||
|
{
|
||||||
|
beatmap.AudioLeadIn = 4000;
|
||||||
|
return base.CreateWorkingBeatmap(beatmap, storyboard);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly Container content;
|
private readonly Container content;
|
||||||
|
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
@ -200,8 +208,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
[Ignore("Fails on github runners if they happen to skip too far forward in time.")]
|
||||||
public void TestUserPauseDuringCooldownTooSoon()
|
public void TestUserPauseDuringCooldownTooSoon()
|
||||||
{
|
{
|
||||||
|
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
|
||||||
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||||
|
|
||||||
pauseAndConfirm();
|
pauseAndConfirm();
|
||||||
@ -213,9 +223,23 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
confirmNotExited();
|
confirmNotExited();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUserPauseDuringIntroSkipsCooldown()
|
||||||
|
{
|
||||||
|
AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
|
||||||
|
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||||
|
|
||||||
|
pauseAndConfirm();
|
||||||
|
|
||||||
|
resume();
|
||||||
|
pauseViaBackAction();
|
||||||
|
confirmPaused();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestQuickExitDuringCooldownTooSoon()
|
public void TestQuickExitDuringCooldownTooSoon()
|
||||||
{
|
{
|
||||||
|
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
|
||||||
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
|
||||||
|
|
||||||
pauseAndConfirm();
|
pauseAndConfirm();
|
||||||
|
@ -15,6 +15,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Input.StateChanges;
|
using osu.Framework.Input.StateChanges;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -42,6 +43,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private GameplayState gameplayState;
|
private GameplayState gameplayState;
|
||||||
|
|
||||||
|
private Drawable content;
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
@ -58,7 +61,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
CachedDependencies = new (Type, object)[] { (typeof(GameplayState), gameplayState) },
|
CachedDependencies = new (Type, object)[] { (typeof(GameplayState), gameplayState) },
|
||||||
Child = createContent(),
|
Child = content = createContent(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -67,10 +70,32 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
public void TestBasic()
|
public void TestBasic()
|
||||||
{
|
{
|
||||||
AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre));
|
AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre));
|
||||||
AddUntilStep("at least one frame recorded", () => replay.Frames.Count > 0);
|
AddUntilStep("at least one frame recorded", () => replay.Frames.Count, () => Is.GreaterThanOrEqualTo(0));
|
||||||
AddUntilStep("position matches", () => playbackManager.ChildrenOfType<Box>().First().Position == recordingManager.ChildrenOfType<Box>().First().Position);
|
AddUntilStep("position matches", () => playbackManager.ChildrenOfType<Box>().First().Position == recordingManager.ChildrenOfType<Box>().First().Position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Explicit("Making this test work in a headless context is high effort due to rate adjustment requirements not aligning with the global fast clock. StopwatchClock usage would need to be replace with a rate adjusting clock that still reads from the parent clock. High effort for a test which likely will not see any changes to covered code for some years.")]
|
||||||
|
public void TestSlowClockStillRecordsFramesInRealtime()
|
||||||
|
{
|
||||||
|
ScheduledDelegate moveFunction = null;
|
||||||
|
|
||||||
|
AddStep("set slow running clock", () =>
|
||||||
|
{
|
||||||
|
var stopwatchClock = new StopwatchClock(true) { Rate = 0.01 };
|
||||||
|
stopwatchClock.Seek(Clock.CurrentTime);
|
||||||
|
|
||||||
|
content.Clock = new FramedClock(stopwatchClock);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("move to center", () => InputManager.MoveMouseTo(recordingManager.ScreenSpaceDrawQuad.Centre));
|
||||||
|
AddStep("much move", () => moveFunction = Scheduler.AddDelayed(() =>
|
||||||
|
InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true));
|
||||||
|
AddWaitStep("move", 10);
|
||||||
|
AddStep("stop move", () => moveFunction.Cancel());
|
||||||
|
AddAssert("at least 60 frames recorded", () => replay.Frames.Count, () => Is.GreaterThanOrEqualTo(60));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHighFrameRate()
|
public void TestHighFrameRate()
|
||||||
{
|
{
|
||||||
@ -81,7 +106,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true));
|
InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true));
|
||||||
AddWaitStep("move", 10);
|
AddWaitStep("move", 10);
|
||||||
AddStep("stop move", () => moveFunction.Cancel());
|
AddStep("stop move", () => moveFunction.Cancel());
|
||||||
AddAssert("at least 60 frames recorded", () => replay.Frames.Count > 60);
|
AddAssert("at least 60 frames recorded", () => replay.Frames.Count, () => Is.GreaterThanOrEqualTo(60));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -97,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true));
|
InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true));
|
||||||
AddWaitStep("move", 10);
|
AddWaitStep("move", 10);
|
||||||
AddStep("stop move", () => moveFunction.Cancel());
|
AddStep("stop move", () => moveFunction.Cancel());
|
||||||
AddAssert("less than 10 frames recorded", () => replay.Frames.Count - initialFrameCount < 10);
|
AddAssert("less than 10 frames recorded", () => replay.Frames.Count - initialFrameCount, () => Is.LessThan(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -114,7 +139,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}, 10, true));
|
}, 10, true));
|
||||||
AddWaitStep("move", 10);
|
AddWaitStep("move", 10);
|
||||||
AddStep("stop move", () => moveFunction.Cancel());
|
AddStep("stop move", () => moveFunction.Cancel());
|
||||||
AddAssert("at least 60 frames recorded", () => replay.Frames.Count > 60);
|
AddAssert("at least 60 frames recorded", () => replay.Frames.Count, () => Is.GreaterThanOrEqualTo(60));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
|
53
osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs
Normal file
53
osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorList.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneSpectatorList : OsuTestScene
|
||||||
|
{
|
||||||
|
private readonly BindableList<SpectatorList.Spectator> spectators = new BindableList<SpectatorList.Spectator>();
|
||||||
|
private readonly Bindable<LocalUserPlayingState> localUserPlayingState = new Bindable<LocalUserPlayingState>();
|
||||||
|
|
||||||
|
private int counter;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasics()
|
||||||
|
{
|
||||||
|
SpectatorList list = null!;
|
||||||
|
AddStep("create spectator list", () => Child = list = new SpectatorList
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Spectators = { BindTarget = spectators },
|
||||||
|
UserPlayingState = { BindTarget = localUserPlayingState }
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing);
|
||||||
|
|
||||||
|
AddRepeatStep("add a user", () =>
|
||||||
|
{
|
||||||
|
int id = Interlocked.Increment(ref counter);
|
||||||
|
spectators.Add(new SpectatorList.Spectator(id, $"User {id}"));
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5);
|
||||||
|
|
||||||
|
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
|
||||||
|
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
|
||||||
|
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
|
||||||
|
|
||||||
|
AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break);
|
||||||
|
AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
|
|
||||||
protected OsuScreenStack IntroStack;
|
protected OsuScreenStack IntroStack;
|
||||||
|
|
||||||
private IntroScreen intro;
|
protected IntroScreen Intro { get; private set; }
|
||||||
|
|
||||||
[Cached(typeof(INotificationOverlay))]
|
[Cached(typeof(INotificationOverlay))]
|
||||||
private NotificationOverlay notifications;
|
private NotificationOverlay notifications;
|
||||||
@ -62,22 +62,9 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
[Test]
|
[Test]
|
||||||
public virtual void TestPlayIntro()
|
public virtual void TestPlayIntro()
|
||||||
{
|
{
|
||||||
AddStep("restart sequence", () =>
|
RestartIntro();
|
||||||
{
|
|
||||||
logo.FinishTransforms();
|
|
||||||
logo.IsTracking = false;
|
|
||||||
|
|
||||||
IntroStack?.Expire();
|
WaitForMenu();
|
||||||
|
|
||||||
Add(IntroStack = new OsuScreenStack
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
});
|
|
||||||
|
|
||||||
IntroStack.Push(intro = CreateScreen());
|
|
||||||
});
|
|
||||||
|
|
||||||
AddUntilStep("wait for menu", () => intro.DidLoadMenu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -103,18 +90,18 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
});
|
});
|
||||||
|
|
||||||
IntroStack.Push(intro = CreateScreen());
|
IntroStack.Push(Intro = CreateScreen());
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("trigger failure", () =>
|
AddStep("trigger failure", () =>
|
||||||
{
|
{
|
||||||
trackResetDelegate = Scheduler.AddDelayed(() =>
|
trackResetDelegate = Scheduler.AddDelayed(() =>
|
||||||
{
|
{
|
||||||
intro.Beatmap.Value.Track.Seek(0);
|
Intro.Beatmap.Value.Track.Seek(0);
|
||||||
}, 0, true);
|
}, 0, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for menu", () => intro.DidLoadMenu);
|
WaitForMenu();
|
||||||
|
|
||||||
if (IntroReliesOnTrack)
|
if (IntroReliesOnTrack)
|
||||||
AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1);
|
AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1);
|
||||||
@ -122,6 +109,29 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddStep("uninstall delegate", () => trackResetDelegate?.Cancel());
|
AddStep("uninstall delegate", () => trackResetDelegate?.Cancel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void RestartIntro()
|
||||||
|
{
|
||||||
|
AddStep("restart sequence", () =>
|
||||||
|
{
|
||||||
|
logo.FinishTransforms();
|
||||||
|
logo.IsTracking = false;
|
||||||
|
|
||||||
|
IntroStack?.Expire();
|
||||||
|
|
||||||
|
Add(IntroStack = new OsuScreenStack
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
});
|
||||||
|
|
||||||
|
IntroStack.Push(Intro = CreateScreen());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void WaitForMenu()
|
||||||
|
{
|
||||||
|
AddUntilStep("wait for menu", () => Intro.DidLoadMenu);
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract IntroScreen CreateScreen();
|
protected abstract IntroScreen CreateScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs
Normal file
16
osu.Game.Tests/Visual/Menus/TestSceneIntroChristmas.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Screens.Menu;
|
||||||
|
using osu.Game.Seasonal;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Menus
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneIntroChristmas : IntroTestScene
|
||||||
|
{
|
||||||
|
protected override bool IntroReliesOnTrack => true;
|
||||||
|
protected override IntroScreen CreateScreen() => new IntroChristmas();
|
||||||
|
}
|
||||||
|
}
|
37
osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs
Normal file
37
osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Screens.Menu;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Menus
|
||||||
|
{
|
||||||
|
[HeadlessTest]
|
||||||
|
[TestFixture]
|
||||||
|
public partial class TestSceneIntroIntegrity : IntroTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public virtual void TestDeletedFilesRestored()
|
||||||
|
{
|
||||||
|
RestartIntro();
|
||||||
|
WaitForMenu();
|
||||||
|
|
||||||
|
AddStep("delete game files unexpectedly", () => LocalStorage.DeleteDirectory("files"));
|
||||||
|
AddStep("reset game beatmap", () => Dependencies.Get<Bindable<WorkingBeatmap>>().Value = new DummyWorkingBeatmap(Audio, null));
|
||||||
|
AddStep("invalidate beatmap from cache", () => Dependencies.Get<IWorkingBeatmapCache>().Invalidate(Intro.Beatmap.Value.BeatmapSetInfo));
|
||||||
|
|
||||||
|
RestartIntro();
|
||||||
|
WaitForMenu();
|
||||||
|
|
||||||
|
AddUntilStep("ensure track is not virtual", () => Intro.Beatmap.Value.Track is TrackBass);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IntroReliesOnTrack => true;
|
||||||
|
protected override IntroScreen CreateScreen() => new IntroTriangles();
|
||||||
|
}
|
||||||
|
}
|
@ -29,9 +29,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||||
|
|
||||||
private LoginOverlay loginOverlay = null!;
|
private LoginOverlay loginOverlay = null!;
|
||||||
|
private OsuConfigManager localConfig = null!;
|
||||||
[Resolved]
|
|
||||||
private OsuConfigManager configManager { get; set; } = null!;
|
|
||||||
|
|
||||||
[Cached(typeof(LocalUserStatisticsProvider))]
|
[Cached(typeof(LocalUserStatisticsProvider))]
|
||||||
private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider();
|
private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider();
|
||||||
@ -39,6 +37,8 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
|
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
|
||||||
|
|
||||||
Child = loginOverlay = new LoginOverlay
|
Child = loginOverlay = new LoginOverlay
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
@ -49,6 +49,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
|
AddStep("reset online state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.Online));
|
||||||
AddStep("show login overlay", () => loginOverlay.Show());
|
AddStep("show login overlay", () => loginOverlay.Show());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +90,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
|
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
|
||||||
|
|
||||||
assertDropdownState(UserAction.Online);
|
assertDropdownState(UserAction.Online);
|
||||||
AddStep("change user state", () => dummyAPI.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb);
|
AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb));
|
||||||
assertDropdownState(UserAction.DoNotDisturb);
|
assertDropdownState(UserAction.DoNotDisturb);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,31 +189,31 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
public void TestUncheckingRememberUsernameClearsIt()
|
public void TestUncheckingRememberUsernameClearsIt()
|
||||||
{
|
{
|
||||||
AddStep("logout", () => API.Logout());
|
AddStep("logout", () => API.Logout());
|
||||||
AddStep("set username", () => configManager.SetValue(OsuSetting.Username, "test_user"));
|
AddStep("set username", () => localConfig.SetValue(OsuSetting.Username, "test_user"));
|
||||||
AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true));
|
AddStep("set remember password", () => localConfig.SetValue(OsuSetting.SavePassword, true));
|
||||||
AddStep("uncheck remember username", () =>
|
AddStep("uncheck remember username", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<SettingsCheckbox>().First());
|
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<SettingsCheckbox>().First());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddAssert("remember username off", () => configManager.Get<bool>(OsuSetting.SaveUsername), () => Is.False);
|
AddAssert("remember username off", () => localConfig.Get<bool>(OsuSetting.SaveUsername), () => Is.False);
|
||||||
AddAssert("remember password off", () => configManager.Get<bool>(OsuSetting.SavePassword), () => Is.False);
|
AddAssert("remember password off", () => localConfig.Get<bool>(OsuSetting.SavePassword), () => Is.False);
|
||||||
AddAssert("username cleared", () => configManager.Get<string>(OsuSetting.Username), () => Is.Empty);
|
AddAssert("username cleared", () => localConfig.Get<string>(OsuSetting.Username), () => Is.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestUncheckingRememberPasswordClearsToken()
|
public void TestUncheckingRememberPasswordClearsToken()
|
||||||
{
|
{
|
||||||
AddStep("logout", () => API.Logout());
|
AddStep("logout", () => API.Logout());
|
||||||
AddStep("set token", () => configManager.SetValue(OsuSetting.Token, "test_token"));
|
AddStep("set token", () => localConfig.SetValue(OsuSetting.Token, "test_token"));
|
||||||
AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true));
|
AddStep("set remember password", () => localConfig.SetValue(OsuSetting.SavePassword, true));
|
||||||
AddStep("uncheck remember token", () =>
|
AddStep("uncheck remember token", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<SettingsCheckbox>().Last());
|
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<SettingsCheckbox>().Last());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
AddAssert("remember password off", () => configManager.Get<bool>(OsuSetting.SavePassword), () => Is.False);
|
AddAssert("remember password off", () => localConfig.Get<bool>(OsuSetting.SavePassword), () => Is.False);
|
||||||
AddAssert("token cleared", () => configManager.Get<string>(OsuSetting.Token), () => Is.Empty);
|
AddAssert("token cleared", () => localConfig.Get<string>(OsuSetting.Token), () => Is.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Seasonal;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Menus
|
||||||
|
{
|
||||||
|
public partial class TestSceneMainMenuSeasonalLighting : OsuTestScene
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmaps { get; set; } = null!;
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("prepare beatmap", () =>
|
||||||
|
{
|
||||||
|
var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH);
|
||||||
|
|
||||||
|
if (setInfo != null)
|
||||||
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo.Value.Beatmaps.First());
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("create lighting", () => Child = new MainMenuSeasonalLighting());
|
||||||
|
|
||||||
|
AddStep("restart beatmap", () =>
|
||||||
|
{
|
||||||
|
Beatmap.Value.Track.Start();
|
||||||
|
Beatmap.Value.Track.Seek(4000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBasic()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,29 +3,71 @@
|
|||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Screens.OnlinePlay.Components;
|
using osu.Game.Screens.OnlinePlay.Components;
|
||||||
using osu.Game.Tests.Visual.OnlinePlay;
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
public partial class TestSceneStarRatingRangeDisplay : OnlinePlayTestScene
|
public partial class TestSceneStarRatingRangeDisplay : OsuTestScene
|
||||||
{
|
{
|
||||||
public override void SetUpSteps()
|
private readonly Room room = new Room();
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.SetUpSteps();
|
base.LoadComplete();
|
||||||
|
|
||||||
AddStep("create display", () =>
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
SelectedRoom.Value = new Room();
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
Child = new StarRatingRangeDisplay(SelectedRoom.Value)
|
Spacing = new Vector2(10),
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
new StarRatingRangeDisplay(room)
|
||||||
Origin = Anchor.Centre
|
{
|
||||||
};
|
Anchor = Anchor.Centre,
|
||||||
});
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(5),
|
||||||
|
},
|
||||||
|
new StarRatingRangeDisplay(room)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(2),
|
||||||
|
},
|
||||||
|
new StarRatingRangeDisplay(room)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Scale = new Vector2(1),
|
||||||
|
},
|
||||||
|
new StarRatingRangeDisplay(room)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Alpha = 0.2f,
|
||||||
|
Scale = new Vector2(5),
|
||||||
|
},
|
||||||
|
new StarRatingRangeDisplay(room)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Alpha = 0.2f,
|
||||||
|
Scale = new Vector2(2),
|
||||||
|
},
|
||||||
|
new StarRatingRangeDisplay(room)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Alpha = 0.2f,
|
||||||
|
Scale = new Vector2(1),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -33,10 +75,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
AddStep("set playlist", () =>
|
AddStep("set playlist", () =>
|
||||||
{
|
{
|
||||||
SelectedRoom.Value!.Playlist =
|
room.Playlist =
|
||||||
[
|
[
|
||||||
new PlaylistItem(new BeatmapInfo { StarRating = min }),
|
new PlaylistItem(new BeatmapInfo { StarRating = min }) { ID = TestResources.GetNextTestID() },
|
||||||
new PlaylistItem(new BeatmapInfo { StarRating = max }),
|
new PlaylistItem(new BeatmapInfo { StarRating = max }) { ID = TestResources.GetNextTestID() },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -58,7 +56,11 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
// First scroll makes volume controls appear, second adjusts volume.
|
// First scroll makes volume controls appear, second adjusts volume.
|
||||||
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 10);
|
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 10);
|
||||||
AddAssert("Volume is still zero", () => Game.Audio.Volume.Value == 0);
|
AddAssert("Volume is still zero", () => Game.Audio.Volume.Value, () => Is.Zero);
|
||||||
|
|
||||||
|
AddStep("Pause", () => InputManager.PressKey(Key.Escape));
|
||||||
|
AddRepeatStep("Adjust volume using mouse wheel", () => InputManager.ScrollVerticalBy(5), 10);
|
||||||
|
AddAssert("Volume is above zero", () => Game.Audio.Volume.Value > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -80,8 +82,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
private void loadToPlayerNonBreakTime()
|
private void loadToPlayerNonBreakTime()
|
||||||
{
|
{
|
||||||
Player player = null;
|
Player? player = null;
|
||||||
Screens.Select.SongSelect songSelect = null;
|
Screens.Select.SongSelect songSelect = null!;
|
||||||
PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
|
PushAndConfirm(() => songSelect = new TestSceneScreenNavigation.TestPlaySongSelect());
|
||||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||||
|
|
||||||
@ -95,7 +97,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
|
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value);
|
AddUntilStep("wait for play time active", () => player!.IsBreakTime.Value, () => Is.False);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components;
|
|||||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osu.Game.Screens.Play.PlayerSettings;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Carousel;
|
using osu.Game.Screens.Select.Carousel;
|
||||||
@ -317,6 +318,92 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen());
|
AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOffsetAdjustDuringPause()
|
||||||
|
{
|
||||||
|
Player player = null;
|
||||||
|
|
||||||
|
Screens.Select.SongSelect songSelect = null;
|
||||||
|
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||||
|
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||||
|
|
||||||
|
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||||
|
|
||||||
|
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() });
|
||||||
|
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () =>
|
||||||
|
{
|
||||||
|
DismissAnyNotifications();
|
||||||
|
player = Game.ScreenStack.CurrentScreen as Player;
|
||||||
|
return player?.IsLoaded == true;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning);
|
||||||
|
checkOffset(0);
|
||||||
|
|
||||||
|
AddStep("adjust offset via keyboard", () => InputManager.Key(Key.Minus));
|
||||||
|
checkOffset(-1);
|
||||||
|
|
||||||
|
AddStep("pause", () => player.ChildrenOfType<GameplayClockContainer>().First().Stop());
|
||||||
|
AddUntilStep("wait for pause", () => player.ChildrenOfType<GameplayClockContainer>().First().IsPaused.Value, () => Is.True);
|
||||||
|
AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus));
|
||||||
|
checkOffset(-1);
|
||||||
|
|
||||||
|
void checkOffset(double offset)
|
||||||
|
{
|
||||||
|
AddUntilStep($"control offset is {offset}", () => this.ChildrenOfType<GameplayOffsetControl>().Single().ChildrenOfType<BeatmapOffsetControl>().Single().Current.Value,
|
||||||
|
() => Is.EqualTo(offset));
|
||||||
|
AddUntilStep($"database offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset,
|
||||||
|
() => Is.EqualTo(offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOffsetAdjustDuringGameplay()
|
||||||
|
{
|
||||||
|
Player player = null;
|
||||||
|
|
||||||
|
Screens.Select.SongSelect songSelect = null;
|
||||||
|
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||||
|
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||||
|
|
||||||
|
AddStep("import beatmap", () => BeatmapImportHelper.LoadOszIntoOsu(Game).WaitSafely());
|
||||||
|
|
||||||
|
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail() });
|
||||||
|
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () =>
|
||||||
|
{
|
||||||
|
DismissAnyNotifications();
|
||||||
|
player = Game.ScreenStack.CurrentScreen as Player;
|
||||||
|
return player?.IsLoaded == true;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning);
|
||||||
|
checkOffset(0);
|
||||||
|
|
||||||
|
AddStep("adjust offset via keyboard", () => InputManager.Key(Key.Minus));
|
||||||
|
checkOffset(-1);
|
||||||
|
|
||||||
|
AddStep("seek beyond 10 seconds", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(10500));
|
||||||
|
AddUntilStep("wait for seek", () => player.ChildrenOfType<GameplayClockContainer>().First().CurrentTime, () => Is.GreaterThan(10600));
|
||||||
|
AddStep("attempt adjust offset via keyboard", () => InputManager.Key(Key.Minus));
|
||||||
|
checkOffset(-1);
|
||||||
|
|
||||||
|
void checkOffset(double offset)
|
||||||
|
{
|
||||||
|
AddUntilStep($"control offset is {offset}", () => this.ChildrenOfType<GameplayOffsetControl>().Single().ChildrenOfType<BeatmapOffsetControl>().Single().Current.Value,
|
||||||
|
() => Is.EqualTo(offset));
|
||||||
|
AddUntilStep($"database offset is {offset}", () => Game.BeatmapManager.QueryBeatmap(b => b.ID == Game.Beatmap.Value.BeatmapInfo.ID)!.UserSettings.Offset,
|
||||||
|
() => Is.EqualTo(offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRetryCountIncrements()
|
public void TestRetryCountIncrements()
|
||||||
{
|
{
|
||||||
@ -355,18 +442,18 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLastScoreNullAfterExitingPlayer()
|
public void TestLastScoreNotNullAfterExitingPlayer()
|
||||||
{
|
{
|
||||||
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
|
AddUntilStep("last play null", getLastPlay, () => Is.Null);
|
||||||
|
|
||||||
var getOriginalPlayer = playToCompletion();
|
var getOriginalPlayer = playToCompletion();
|
||||||
|
|
||||||
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
|
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
|
||||||
AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
|
AddUntilStep("last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
|
||||||
|
|
||||||
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
|
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
|
||||||
AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit());
|
AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit());
|
||||||
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
|
AddUntilStep("last play not null", getLastPlay, () => Is.Not.Null);
|
||||||
|
|
||||||
ScoreInfo getLastPlay() => Game.Dependencies.Get<SessionStatics>().Get<ScoreInfo>(Static.LastLocalUserScore);
|
ScoreInfo getLastPlay() => Game.Dependencies.Get<SessionStatics>().Get<ScoreInfo>(Static.LastLocalUserScore);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
@ -102,6 +103,77 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get<SkinManager>().CurrentSkin.Value.SkinInfo.Value.Protected);
|
AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get<SkinManager>().CurrentSkin.Value.SkinInfo.Value.Protected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMutateProtectedSkinFromMainMenu_UndoToInitialStateIsCorrect()
|
||||||
|
{
|
||||||
|
AddStep("set default skin", () => Game.Dependencies.Get<SkinManager>().CurrentSkinInfo.SetDefault());
|
||||||
|
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||||
|
|
||||||
|
openSkinEditor();
|
||||||
|
AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get<SkinManager>().CurrentSkin.Value.SkinInfo.Value.Protected);
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () =>
|
||||||
|
{
|
||||||
|
DismissAnyNotifications();
|
||||||
|
return Game.ScreenStack.CurrentScreen is Player;
|
||||||
|
});
|
||||||
|
|
||||||
|
string state = string.Empty;
|
||||||
|
|
||||||
|
AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType<ArgonAccuracyCounter>().Any(counter => counter.Position != new Vector2()));
|
||||||
|
AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType<ArgonAccuracyCounter>().First().CreateSerialisedInfo()));
|
||||||
|
AddStep("add any component", () => Game.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick());
|
||||||
|
AddStep("undo", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Z);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddUntilStep("only one accuracy meter left",
|
||||||
|
() => Game.ChildrenOfType<Player>().Single().ChildrenOfType<ArgonAccuracyCounter>().Count(),
|
||||||
|
() => Is.EqualTo(1));
|
||||||
|
AddAssert("accuracy meter state unchanged",
|
||||||
|
() => JsonConvert.SerializeObject(Game.ChildrenOfType<ArgonAccuracyCounter>().First().CreateSerialisedInfo()),
|
||||||
|
() => Is.EqualTo(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMutateProtectedSkinFromPlayer_UndoToInitialStateIsCorrect()
|
||||||
|
{
|
||||||
|
AddStep("set default skin", () => Game.Dependencies.Get<SkinManager>().CurrentSkinInfo.SetDefault());
|
||||||
|
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||||
|
advanceToSongSelect();
|
||||||
|
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() });
|
||||||
|
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () =>
|
||||||
|
{
|
||||||
|
DismissAnyNotifications();
|
||||||
|
return Game.ScreenStack.CurrentScreen is Player;
|
||||||
|
});
|
||||||
|
openSkinEditor();
|
||||||
|
|
||||||
|
string state = string.Empty;
|
||||||
|
|
||||||
|
AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType<ArgonAccuracyCounter>().Any(counter => counter.Position != new Vector2()));
|
||||||
|
AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType<ArgonAccuracyCounter>().First().CreateSerialisedInfo()));
|
||||||
|
AddStep("add any component", () => Game.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick());
|
||||||
|
AddStep("undo", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Z);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddUntilStep("only one accuracy meter left",
|
||||||
|
() => Game.ChildrenOfType<Player>().Single().ChildrenOfType<ArgonAccuracyCounter>().Count(),
|
||||||
|
() => Is.EqualTo(1));
|
||||||
|
AddAssert("accuracy meter state unchanged",
|
||||||
|
() => JsonConvert.SerializeObject(Game.ChildrenOfType<ArgonAccuracyCounter>().First().CreateSerialisedInfo()),
|
||||||
|
() => Is.EqualTo(state));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestComponentsDeselectedOnSkinEditorHide()
|
public void TestComponentsDeselectedOnSkinEditorHide()
|
||||||
{
|
{
|
||||||
|
@ -8,7 +8,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -23,17 +23,23 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Cached(typeof(IChannelPostTarget))]
|
[Cached(typeof(IChannelPostTarget))]
|
||||||
private PostTarget postTarget { get; set; }
|
private PostTarget postTarget { get; set; }
|
||||||
|
|
||||||
private DummyAPIAccess api => (DummyAPIAccess)API;
|
private SessionStatics session = null!;
|
||||||
|
|
||||||
public TestSceneNowPlayingCommand()
|
public TestSceneNowPlayingCommand()
|
||||||
{
|
{
|
||||||
Add(postTarget = new PostTarget());
|
Add(postTarget = new PostTarget());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Dependencies.Cache(session = new SessionStatics());
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestGenericActivity()
|
public void TestGenericActivity()
|
||||||
{
|
{
|
||||||
AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room()));
|
AddStep("Set activity", () => session.SetValue<UserActivity>(Static.UserOnlineActivity, new UserActivity.InLobby(new Room())));
|
||||||
|
|
||||||
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
|
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
|
||||||
|
|
||||||
@ -43,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestEditActivity()
|
public void TestEditActivity()
|
||||||
{
|
{
|
||||||
AddStep("Set activity", () => api.Activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
|
AddStep("Set activity", () => session.SetValue<UserActivity>(Static.UserOnlineActivity, new UserActivity.EditingBeatmap(new BeatmapInfo())));
|
||||||
|
|
||||||
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
|
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
|
||||||
|
|
||||||
@ -53,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestPlayActivity()
|
public void TestPlayActivity()
|
||||||
{
|
{
|
||||||
AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo));
|
AddStep("Set activity", () => session.SetValue<UserActivity>(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)));
|
||||||
|
|
||||||
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
|
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));
|
||||||
|
|
||||||
@ -64,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[TestCase(false)]
|
[TestCase(false)]
|
||||||
public void TestLinkPresence(bool hasOnlineId)
|
public void TestLinkPresence(bool hasOnlineId)
|
||||||
{
|
{
|
||||||
AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(new Room()));
|
AddStep("Set activity", () => session.SetValue<UserActivity>(Static.UserOnlineActivity, new UserActivity.InLobby(new Room())));
|
||||||
|
|
||||||
AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)
|
AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)
|
||||||
{
|
{
|
||||||
@ -82,7 +88,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestModPresence()
|
public void TestModPresence()
|
||||||
{
|
{
|
||||||
AddStep("Set activity", () => api.Activity.Value = new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo));
|
AddStep("Set activity", () => session.SetValue<UserActivity>(Static.UserOnlineActivity, new UserActivity.InSoloGame(new BeatmapInfo(), new OsuRuleset().RulesetInfo)));
|
||||||
|
|
||||||
AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod<ModHidden>() });
|
AddStep("Add Hidden mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod<ModHidden>() });
|
||||||
|
|
||||||
|
@ -62,10 +62,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
CountryCode = countryCode,
|
CountryCode = countryCode,
|
||||||
CoverUrl = cover,
|
CoverUrl = cover,
|
||||||
Colour = color ?? "000000",
|
Colour = color ?? "000000",
|
||||||
Status =
|
IsOnline = true
|
||||||
{
|
|
||||||
Value = UserStatus.Online
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return new ClickableAvatar(user, showPanel)
|
return new ClickableAvatar(user, showPanel)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user