mirror of
https://github.com/ppy/osu.git
synced 2025-01-18 10:53:21 +08:00
Merge branch 'master' into colour-for-friend-lb
This commit is contained in:
commit
3c913c9f6d
@ -51,8 +51,11 @@ dotnet_diagnostic.IDE1006.severity = warning
|
||||
# Too many noisy warnings for parsing/formatting numbers
|
||||
dotnet_diagnostic.CA1305.severity = none
|
||||
|
||||
# messagepack complains about "osu" not being title cased due to reserved words
|
||||
dotnet_diagnostic.CS8981.severity = none
|
||||
|
||||
# CA1507: Use nameof to express symbol names
|
||||
# Flaggs serialization name attributes
|
||||
# Flags serialization name attributes
|
||||
dotnet_diagnostic.CA1507.severity = suggestion
|
||||
|
||||
# CA1806: Do not ignore method results
|
||||
|
@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu!
|
||||
|
||||
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
|
||||
|
||||
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024.
|
||||
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
|
||||
|
||||
## Developing a custom ruleset
|
||||
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||
|
@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(X, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(X, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||
|
@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1220.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1224.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -67,7 +67,12 @@ namespace osu.Desktop
|
||||
{
|
||||
try
|
||||
{
|
||||
stableInstallPath = getStableInstallPathFromRegistry();
|
||||
stableInstallPath = getStableInstallPathFromRegistry("osustable.File.osz");
|
||||
|
||||
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
||||
return stableInstallPath;
|
||||
|
||||
stableInstallPath = getStableInstallPathFromRegistry("osu!");
|
||||
|
||||
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
||||
return stableInstallPath;
|
||||
@ -89,9 +94,9 @@ namespace osu.Desktop
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private string? getStableInstallPathFromRegistry()
|
||||
private string? getStableInstallPathFromRegistry(string progId)
|
||||
{
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey(progId))
|
||||
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
}
|
||||
|
||||
@ -134,7 +139,6 @@ namespace osu.Desktop
|
||||
if (iconStream != null)
|
||||
host.Window.SetIconFromStream(iconStream);
|
||||
|
||||
host.Window.CursorState |= CursorState.Hidden;
|
||||
host.Window.Title = Name;
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ namespace osu.Desktop.Windows
|
||||
public static class WindowsAssociationManager
|
||||
{
|
||||
private const string software_classes = @"Software\Classes";
|
||||
private const string software_registered_applications = @"Software\RegisteredApplications";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the icon.
|
||||
@ -36,7 +37,11 @@ namespace osu.Desktop.Windows
|
||||
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
||||
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||
/// </summary>
|
||||
private const string program_id_prefix = "osu.File";
|
||||
private const string program_id_file_prefix = "osu.File";
|
||||
|
||||
private const string program_id_protocol_prefix = "osu.Uri";
|
||||
|
||||
private static readonly ApplicationCapability application_capability = new ApplicationCapability(@"osu", @"Software\ppy\osu\Capabilities", "osu!(lazer)");
|
||||
|
||||
private static readonly FileAssociation[] file_associations =
|
||||
{
|
||||
@ -56,14 +61,13 @@ namespace osu.Desktop.Windows
|
||||
/// Installs file and URI associations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void InstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -76,17 +80,13 @@ namespace osu.Desktop.Windows
|
||||
/// Updates associations with latest definitions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void UpdateAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
|
||||
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
|
||||
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -95,11 +95,19 @@ namespace osu.Desktop.Windows
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateDescriptions(LocalisationManager localisationManager)
|
||||
// TODO: call this sometime.
|
||||
public static void LocaliseDescriptions(LocalisationManager localisationManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
updateDescriptions(localisationManager);
|
||||
application_capability.LocaliseDescription(localisationManager);
|
||||
|
||||
foreach (var association in file_associations)
|
||||
association.LocaliseDescription(localisationManager);
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.LocaliseDescription(localisationManager);
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -112,6 +120,8 @@ namespace osu.Desktop.Windows
|
||||
{
|
||||
try
|
||||
{
|
||||
application_capability.Uninstall();
|
||||
|
||||
foreach (var association in file_associations)
|
||||
association.Uninstall();
|
||||
|
||||
@ -133,22 +143,16 @@ namespace osu.Desktop.Windows
|
||||
/// </summary>
|
||||
private static void updateAssociations()
|
||||
{
|
||||
application_capability.Install();
|
||||
|
||||
foreach (var association in file_associations)
|
||||
association.Install();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Install();
|
||||
}
|
||||
|
||||
private static void updateDescriptions(LocalisationManager? localisation)
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString();
|
||||
application_capability.RegisterFileAssociations(file_associations);
|
||||
application_capability.RegisterUriAssociations(uri_associations);
|
||||
}
|
||||
|
||||
#region Native interop
|
||||
@ -174,9 +178,87 @@ namespace osu.Desktop.Windows
|
||||
|
||||
#endregion
|
||||
|
||||
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
|
||||
private class ApplicationCapability
|
||||
{
|
||||
private string programId => $@"{program_id_prefix}{Extension}";
|
||||
private string uniqueName { get; }
|
||||
private string capabilityPath { get; }
|
||||
private LocalisableString description { get; }
|
||||
|
||||
public ApplicationCapability(string uniqueName, string capabilityPath, LocalisableString description)
|
||||
{
|
||||
this.uniqueName = uniqueName;
|
||||
this.capabilityPath = capabilityPath;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an application capability according to <see href="https://learn.microsoft.com/en-us/windows/win32/shell/default-programs#registering-an-application-for-use-with-default-programs">
|
||||
/// Registering an Application for Use with Default Programs</see>.
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using (var capability = Registry.CurrentUser.CreateSubKey(capabilityPath))
|
||||
{
|
||||
capability.SetValue(@"ApplicationDescription", description.ToString());
|
||||
}
|
||||
|
||||
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
|
||||
registeredApplications?.SetValue(uniqueName, capabilityPath);
|
||||
}
|
||||
|
||||
public void RegisterFileAssociations(FileAssociation[] associations)
|
||||
{
|
||||
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
|
||||
if (capability == null) return;
|
||||
|
||||
using var fileAssociations = capability.CreateSubKey(@"FileAssociations");
|
||||
|
||||
foreach (var association in associations)
|
||||
fileAssociations.SetValue(association.Extension, association.ProgramId);
|
||||
}
|
||||
|
||||
public void RegisterUriAssociations(UriAssociation[] associations)
|
||||
{
|
||||
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
|
||||
if (capability == null) return;
|
||||
|
||||
using var urlAssociations = capability.CreateSubKey(@"UrlAssociations");
|
||||
|
||||
foreach (var association in associations)
|
||||
urlAssociations.SetValue(association.Protocol, association.ProgramId);
|
||||
}
|
||||
|
||||
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||
{
|
||||
using (var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true))
|
||||
{
|
||||
capability?.SetValue(@"ApplicationDescription", localisationManager.GetLocalisedString(description));
|
||||
}
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
|
||||
registeredApplications?.DeleteValue(uniqueName, throwOnMissingValue: false);
|
||||
|
||||
Registry.CurrentUser.DeleteSubKeyTree(capabilityPath, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
|
||||
private class FileAssociation
|
||||
{
|
||||
public string ProgramId => $@"{program_id_file_prefix}{Extension}";
|
||||
|
||||
public string Extension { get; }
|
||||
private LocalisableString description { get; }
|
||||
private string iconPath { get; }
|
||||
|
||||
public FileAssociation(string extension, LocalisableString description, string iconPath)
|
||||
{
|
||||
Extension = extension;
|
||||
this.description = description;
|
||||
this.iconPath = iconPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||
@ -187,10 +269,12 @@ namespace osu.Desktop.Windows
|
||||
if (classes == null) return;
|
||||
|
||||
// register a program id for the given extension
|
||||
using (var programKey = classes.CreateSubKey(programId))
|
||||
using (var programKey = classes.CreateSubKey(ProgramId))
|
||||
{
|
||||
programKey.SetValue(null, description.ToString());
|
||||
|
||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
defaultIconKey.SetValue(null, iconPath);
|
||||
|
||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
@ -198,23 +282,25 @@ namespace osu.Desktop.Windows
|
||||
|
||||
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||
{
|
||||
// set ourselves as the default program
|
||||
extensionKey.SetValue(null, programId);
|
||||
// Clear out our existing default ProgramID. Default programs in Windows are handled internally by Explorer,
|
||||
// so having it here is just confusing and may override user preferences.
|
||||
if (extensionKey.GetValue(null) is string s && s == ProgramId)
|
||||
extensionKey.SetValue(null, string.Empty);
|
||||
|
||||
// add to the open with dialog
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
||||
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey.SetValue(programId, string.Empty);
|
||||
openWithKey.SetValue(ProgramId, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var programKey = classes.OpenSubKey(programId, true))
|
||||
programKey?.SetValue(null, description);
|
||||
using (var programKey = classes.OpenSubKey(ProgramId, true))
|
||||
programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -227,26 +313,34 @@ namespace osu.Desktop.Windows
|
||||
|
||||
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
||||
{
|
||||
// clear our default association so that Explorer doesn't show the raw programId to users
|
||||
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
|
||||
if (extensionKey?.GetValue(null) is string s && s == programId)
|
||||
extensionKey.SetValue(null, string.Empty);
|
||||
|
||||
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
|
||||
openWithKey?.DeleteValue(ProgramId, throwOnMissingValue: false);
|
||||
}
|
||||
|
||||
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
|
||||
classes.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
|
||||
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
|
||||
private class UriAssociation
|
||||
{
|
||||
/// <summary>
|
||||
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
||||
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public const string URL_PROTOCOL = @"URL Protocol";
|
||||
private const string url_protocol = @"URL Protocol";
|
||||
|
||||
public string Protocol { get; }
|
||||
private LocalisableString description { get; }
|
||||
private string iconPath { get; }
|
||||
|
||||
public UriAssociation(string protocol, LocalisableString description, string iconPath)
|
||||
{
|
||||
Protocol = protocol;
|
||||
this.description = description;
|
||||
this.iconPath = iconPath;
|
||||
}
|
||||
|
||||
public string ProgramId => $@"{program_id_protocol_prefix}.{Protocol}";
|
||||
|
||||
/// <summary>
|
||||
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
@ -258,29 +352,38 @@ namespace osu.Desktop.Windows
|
||||
|
||||
using (var protocolKey = classes.CreateSubKey(Protocol))
|
||||
{
|
||||
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
|
||||
protocolKey.SetValue(null, $@"URL:{description}");
|
||||
protocolKey.SetValue(url_protocol, string.Empty);
|
||||
|
||||
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
// clear out old data
|
||||
protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false);
|
||||
protocolKey.DeleteSubKeyTree(@"Shell", throwOnMissingSubKey: false);
|
||||
}
|
||||
|
||||
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
// register a program id for the given protocol
|
||||
using (var programKey = classes.CreateSubKey(ProgramId))
|
||||
{
|
||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, iconPath);
|
||||
|
||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||
protocolKey?.SetValue(null, $@"URL:{description}");
|
||||
protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
||||
classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,9 +24,9 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.915" />
|
||||
<PackageReference Include="Velopack" Version="0.0.1053" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
@ -9,7 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
||||
{
|
||||
private JuiceStream hitObject;
|
||||
private JuiceStream hitObject = null!;
|
||||
|
||||
private readonly ManualClock manualClock = new ManualClock();
|
||||
|
||||
@ -193,6 +191,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addVertexCheckStep(1, 0, times[0], positions[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeletingSecondVertexDeletesEntireJuiceStream()
|
||||
{
|
||||
double[] times = { 100, 400 };
|
||||
float[] positions = { 100, 150 };
|
||||
addBlueprintStep(times, positions);
|
||||
|
||||
addDeleteVertexSteps(times[1], positions[1]);
|
||||
AddAssert("juice stream deleted", () => EditorBeatmap.HitObjects, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVertexResampling()
|
||||
{
|
||||
|
@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
}));
|
||||
}
|
||||
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
public virtual void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
// The SV setting may need to be changed for the current path.
|
||||
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
||||
|
@ -138,5 +138,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
EditorBeatmap?.EndChange();
|
||||
}
|
||||
|
||||
public override void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
base.UpdateHitObjectFromPath(hitObject);
|
||||
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
||||
EditorBeatmap?.Remove(hitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,10 +88,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
switch (PlacementActive)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
if (!(result.Time is double snappedTime)) return;
|
||||
|
||||
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||
HitObject.StartTime = snappedTime;
|
||||
if (result.Time is double snappedTime)
|
||||
HitObject.StartTime = snappedTime;
|
||||
break;
|
||||
|
||||
case PlacementState.Active:
|
||||
@ -107,21 +106,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
|
||||
|
||||
updateHitObjectFromPath();
|
||||
}
|
||||
if (lastEditablePathId != editablePath.PathId)
|
||||
editablePath.UpdateHitObjectFromPath(HitObject);
|
||||
lastEditablePathId = editablePath.PathId;
|
||||
|
||||
private void updateHitObjectFromPath()
|
||||
{
|
||||
if (lastEditablePathId == editablePath.PathId)
|
||||
return;
|
||||
|
||||
editablePath.UpdateHitObjectFromPath(HitObject);
|
||||
ApplyDefaultsToHitObject();
|
||||
|
||||
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
||||
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
||||
|
||||
lastEditablePathId = editablePath.PathId;
|
||||
}
|
||||
|
||||
private double positionToTime(float relativeYPosition)
|
||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
||||
protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
|
||||
=> base.CreateTernaryButtons()
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||
|
||||
|
@ -210,11 +210,27 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
||||
|
||||
float IHasXPosition.X => OriginalX;
|
||||
float IHasXPosition.X
|
||||
{
|
||||
get => OriginalX;
|
||||
set => OriginalX = value;
|
||||
}
|
||||
|
||||
float IHasYPosition.Y => LegacyConvertedY;
|
||||
float IHasYPosition.Y
|
||||
{
|
||||
get => LegacyConvertedY;
|
||||
set => LegacyConvertedY = value;
|
||||
}
|
||||
|
||||
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
|
||||
Vector2 IHasPosition.Position
|
||||
{
|
||||
get => new Vector2(OriginalX, LegacyConvertedY);
|
||||
set
|
||||
{
|
||||
((IHasXPosition)this).X = value.X;
|
||||
((IHasYPosition)this).Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
|
||||
#region LegacyBeatmapEncoder
|
||||
|
||||
float IHasXPosition.X => Column;
|
||||
float IHasXPosition.X
|
||||
{
|
||||
get => Column;
|
||||
set => Column = (int)value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
@ -261,6 +263,163 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuickDeleteOnUnselectedControlPointOnlyRemovesThatControlPoint()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
new PathControlPoint(new Vector2(100)),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("also select third node", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("quick-delete fourth node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(3));
|
||||
InputManager.Click(MouseButton.Middle);
|
||||
});
|
||||
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("slider path has 3 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuickDeleteOnSelectedControlPointRemovesEntireSelection()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
new PathControlPoint(new Vector2(100)),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("also select third node", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("quick-delete second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Middle);
|
||||
});
|
||||
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("slider path has 2 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDragMarkerDoesNotBlockControlPointContextMenu()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(50, 100)),
|
||||
new PathControlPoint(new Vector2(145, 100)),
|
||||
},
|
||||
ExpectedDistance = { Value = 162.62 }
|
||||
},
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select last node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().Last());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("right click node", () => InputManager.Click(MouseButton.Right));
|
||||
AddUntilStep("context menu open", () => this.ChildrenOfType<ContextMenuContainer>().Single().ChildrenOfType<Menu>().All(m => m.State == MenuState.Open));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDragMarkerBlocksSelectionOfObjectsUnderneath()
|
||||
{
|
||||
var firstSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(10, 50),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
var secondSlider = new Slider
|
||||
{
|
||||
StartTime = 500,
|
||||
Position = new Vector2(200, 0),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(-100, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
AddStep("select second slider", () => EditorBeatmap.SelectedHitObjects.Add(secondSlider));
|
||||
|
||||
AddStep("move to marker", () =>
|
||||
{
|
||||
var marker = this.ChildrenOfType<SliderEndDragMarker>().First();
|
||||
var position = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
|
||||
InputManager.MoveMouseTo(position);
|
||||
});
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second slider still selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondSlider));
|
||||
}
|
||||
|
||||
private ComposeBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||
|
||||
|
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -137,11 +137,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// <summary>
|
||||
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>Whether any change actually took place.</returns>
|
||||
public bool DeleteSelected()
|
||||
{
|
||||
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
|
||||
|
||||
if (!Delete(toRemove))
|
||||
return false;
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
piece.IsSelected.Value = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the specified <see cref="PathControlPoint"/>s.
|
||||
/// </summary>
|
||||
/// <returns>Whether any change actually took place.</returns>
|
||||
public bool Delete(List<PathControlPoint> toRemove)
|
||||
{
|
||||
// Ensure that there are any points to be deleted
|
||||
if (toRemove.Count == 0)
|
||||
return false;
|
||||
@ -149,11 +165,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
changeHandler?.BeginChange();
|
||||
RemoveControlPointsRequested?.Invoke(toRemove);
|
||||
changeHandler?.EndChange();
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
piece.IsSelected.Value = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
@ -76,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
protected override bool OnClick(ClickEvent e) => e.Button == MouseButton.Left;
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
|
@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (hoveredControlPoint == null)
|
||||
return false;
|
||||
|
||||
hoveredControlPoint.IsSelected.Value = true;
|
||||
ControlPointVisualiser?.DeleteSelected();
|
||||
if (hoveredControlPoint.IsSelected.Value)
|
||||
ControlPointVisualiser?.DeleteSelected();
|
||||
else
|
||||
ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -623,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0)
|
||||
return true;
|
||||
|
||||
if (ControlPointVisualiser == null)
|
||||
|
@ -53,9 +53,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
||||
protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
|
||||
=> base.CreateTernaryButtons()
|
||||
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
|
||||
.Append(new DrawableTernaryButton
|
||||
{
|
||||
Current = rectangularGridSnapToggle,
|
||||
Description = "Grid Snap",
|
||||
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap },
|
||||
})
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||
|
||||
private BindableList<HitObject> selectedHitObjects;
|
||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
/// <summary>
|
||||
/// How early before a hitobject's start time to trigger a hit.
|
||||
/// </summary>
|
||||
private const float relax_leniency = 3;
|
||||
public const float RELAX_LENIENCY = 12;
|
||||
|
||||
private bool isDownState;
|
||||
private bool wasLeft;
|
||||
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
||||
{
|
||||
// we are not yet close enough to the object.
|
||||
if (time < h.HitObject.StartTime - relax_leniency)
|
||||
if (time < h.HitObject.StartTime - RELAX_LENIENCY)
|
||||
break;
|
||||
|
||||
// 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,
|
||||
// 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);
|
||||
HeadCircle.SuppressHitAnimations();
|
||||
|
||||
foreach (var repeat in repeatContainer)
|
||||
repeat.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()
|
||||
{
|
||||
UpdateState(ArmedState.Hit);
|
||||
HeadCircle.RestoreHitAnimations();
|
||||
|
||||
foreach (var repeat in repeatContainer)
|
||||
repeat.RestoreHitAnimations();
|
||||
|
||||
TailCircle.RestoreHitAnimations();
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
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;
|
||||
|
||||
|
@ -5,12 +5,12 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
@ -75,44 +75,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
||||
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 scale_amount = 1.3f;
|
||||
|
||||
const double move_out_duration = 35;
|
||||
const double move_in_duration = 250;
|
||||
const double total = 300;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
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;
|
||||
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
|
||||
|
||||
case ArmedState.Hit:
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
this.ScaleTo(1.5f, animDuration, Easing.Out);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (loopCurrentTime < move_out_duration)
|
||||
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
|
||||
else
|
||||
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)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableRepeat.IsNotNull())
|
||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
if (loopCurrentTime < move_out_duration)
|
||||
side.X = Interpolation.ValueAt(loopCurrentTime, 0, move_distance, 0, move_out_duration, Easing.Out);
|
||||
else
|
||||
side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 0, move_out_duration, move_out_duration + move_in_duration, Easing.Out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,10 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
@ -40,37 +40,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
private void load(DrawableHitObject drawableObject)
|
||||
{
|
||||
drawableRepeat = (DrawableSliderRepeat)drawableObject;
|
||||
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
||||
protected override void Update()
|
||||
{
|
||||
const double move_out_duration = 35;
|
||||
const double move_in_duration = 250;
|
||||
const double total = 300;
|
||||
base.Update();
|
||||
|
||||
switch (state)
|
||||
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
InternalChild.ScaleTo(1.3f, move_out_duration, 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;
|
||||
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
|
||||
{
|
||||
const float scale_amount = 1.3f;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
const double move_out_duration = 35;
|
||||
const double move_in_duration = 250;
|
||||
const double total = 300;
|
||||
|
||||
if (drawableRepeat.IsNotNull())
|
||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,13 +61,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
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:
|
||||
//
|
||||
// - Beatmap provides `hitcircle`
|
||||
// - User skin provides `sliderstartcircle`
|
||||
//
|
||||
// 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;
|
||||
|
||||
// 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.
|
||||
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,
|
||||
Origin = Anchor.Centre,
|
||||
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
Anchor = 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,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -9,10 +9,12 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
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;
|
||||
|
||||
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
|
||||
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.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);
|
||||
}
|
||||
|
||||
@ -80,36 +80,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
||||
protected override void Update()
|
||||
{
|
||||
const double duration = 300;
|
||||
const float rotation = 5.625f;
|
||||
base.Update();
|
||||
|
||||
switch (state)
|
||||
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
if (shouldRotate)
|
||||
{
|
||||
InternalChild.ScaleTo(1.3f)
|
||||
.RotateTo(rotation)
|
||||
.Then()
|
||||
.ScaleTo(1f, duration)
|
||||
.RotateTo(-rotation, duration)
|
||||
.Loop();
|
||||
}
|
||||
else
|
||||
{
|
||||
InternalChild.ScaleTo(1.3f).Then()
|
||||
.ScaleTo(1f, duration, Easing.Out)
|
||||
.Loop();
|
||||
}
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
arrow.Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.4f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||
}
|
||||
else
|
||||
{
|
||||
const double duration = 300;
|
||||
const float rotation = 5.625f;
|
||||
|
||||
break;
|
||||
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration;
|
||||
|
||||
case ArmedState.Hit:
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
InternalChild.ScaleTo(1.4f, animDuration, Easing.Out);
|
||||
break;
|
||||
// Reference: https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameplayElements/HitObjects/Osu/HitCircleSliderEnd.cs#L79-L96
|
||||
if (shouldRotate)
|
||||
{
|
||||
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())
|
||||
{
|
||||
drawableRepeat.HitObjectApplied -= onHitObjectApplied;
|
||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual;
|
||||
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));
|
||||
}
|
||||
|
||||
[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]
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
[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.
@ -7,18 +7,18 @@ using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
||||
using osu.Game.Screens.Edit.GameplayTest;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -42,14 +42,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private BeatmapSetInfo importedBeatmapSet;
|
||||
|
||||
private Bindable<float> editorDim;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
editorDim = config.GetBindable<float>(OsuSetting.EditorDim);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
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);
|
||||
AddStep("exit player", () => editorPlayer.Exit());
|
||||
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
|
||||
AddUntilStep("background has correct params", () =>
|
||||
{
|
||||
// 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;
|
||||
});
|
||||
AddUntilStep("background is correct", () => this.ChildrenOfType<BackgroundScreenStack>().Single().CurrentScreen is EditorBackgroundScreen);
|
||||
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
|
||||
}
|
||||
|
||||
@ -113,20 +97,41 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("exit player", () => editorPlayer.Exit());
|
||||
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
|
||||
AddUntilStep("background has correct params", () =>
|
||||
{
|
||||
// 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;
|
||||
});
|
||||
AddUntilStep("background is correct", () => this.ChildrenOfType<BackgroundScreenStack>().Single().CurrentScreen is EditorBackgroundScreen);
|
||||
|
||||
AddStep("start track", () => EditorClock.Start());
|
||||
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(22000)] // chosen to be in the middle of the last spinner
|
||||
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
|
||||
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = Guid.NewGuid().ToString());
|
||||
|
||||
AddStep("start playing track", () => InputManager.Key(Key.Space));
|
||||
AddStep("click test gameplay button", () =>
|
||||
{
|
||||
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
|
||||
@ -185,11 +191,13 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||
AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning);
|
||||
|
||||
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||
|
||||
EditorPlayer 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);
|
||||
|
||||
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor);
|
||||
|
@ -27,18 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Create control", () =>
|
||||
{
|
||||
Child = new PlayerSettingsGroup("Some settings")
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
offsetControl = new BeatmapOffsetControl()
|
||||
}
|
||||
};
|
||||
});
|
||||
recreateControl();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -123,13 +112,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestCalibrationFromZero()
|
||||
{
|
||||
ScoreInfo referenceScore = null!;
|
||||
const double average_error = -4.5;
|
||||
|
||||
AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0);
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
AddStep("Set reference score", () =>
|
||||
{
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
offsetControl.ReferenceScore.Value = referenceScore = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
|
||||
BeatmapInfo = Beatmap.Value.BeatmapInfo,
|
||||
@ -143,6 +133,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
|
||||
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
|
||||
recreateControl();
|
||||
AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore);
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -251,5 +245,21 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
|
||||
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()
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
@ -42,6 +43,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private GameplayState gameplayState;
|
||||
|
||||
private Drawable content;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
@ -58,7 +61,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
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()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
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));
|
||||
AddWaitStep("move", 10);
|
||||
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]
|
||||
@ -97,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(-1, 0)), 10, true));
|
||||
AddWaitStep("move", 10);
|
||||
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]
|
||||
@ -114,7 +139,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}, 10, true));
|
||||
AddWaitStep("move", 10);
|
||||
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()
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
|
||||
protected OsuScreenStack IntroStack;
|
||||
|
||||
private IntroScreen intro;
|
||||
protected IntroScreen Intro { get; private set; }
|
||||
|
||||
[Cached(typeof(INotificationOverlay))]
|
||||
private NotificationOverlay notifications;
|
||||
@ -62,22 +62,9 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
[Test]
|
||||
public virtual void TestPlayIntro()
|
||||
{
|
||||
AddStep("restart sequence", () =>
|
||||
{
|
||||
logo.FinishTransforms();
|
||||
logo.IsTracking = false;
|
||||
RestartIntro();
|
||||
|
||||
IntroStack?.Expire();
|
||||
|
||||
Add(IntroStack = new OsuScreenStack
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
IntroStack.Push(intro = CreateScreen());
|
||||
});
|
||||
|
||||
AddUntilStep("wait for menu", () => intro.DidLoadMenu);
|
||||
WaitForMenu();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -103,18 +90,18 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
|
||||
IntroStack.Push(intro = CreateScreen());
|
||||
IntroStack.Push(Intro = CreateScreen());
|
||||
});
|
||||
|
||||
AddStep("trigger failure", () =>
|
||||
{
|
||||
trackResetDelegate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
intro.Beatmap.Value.Track.Seek(0);
|
||||
Intro.Beatmap.Value.Track.Seek(0);
|
||||
}, 0, true);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for menu", () => intro.DidLoadMenu);
|
||||
WaitForMenu();
|
||||
|
||||
if (IntroReliesOnTrack)
|
||||
AddUntilStep("wait for notification", () => notifications.UnreadCount.Value == 1);
|
||||
@ -122,6 +109,29 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
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();
|
||||
}
|
||||
}
|
@ -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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Rooms;
|
||||
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
|
||||
{
|
||||
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();
|
||||
|
||||
Child = new StarRatingRangeDisplay(SelectedRoom.Value)
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
};
|
||||
});
|
||||
new StarRatingRangeDisplay(room)
|
||||
{
|
||||
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]
|
||||
@ -33,10 +75,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("set playlist", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new BeatmapInfo { StarRating = min }),
|
||||
new PlaylistItem(new BeatmapInfo { StarRating = max }),
|
||||
new PlaylistItem(new BeatmapInfo { StarRating = min }) { ID = TestResources.GetNextTestID() },
|
||||
new PlaylistItem(new BeatmapInfo { StarRating = max }) { ID = TestResources.GetNextTestID() },
|
||||
];
|
||||
});
|
||||
}
|
||||
|
@ -355,18 +355,18 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
}
|
||||
|
||||
[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();
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public void TestComponentsDeselectedOnSkinEditorHide()
|
||||
{
|
||||
|
@ -12,7 +12,6 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
@ -85,6 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestPresentedBeatmapIsRecommended()
|
||||
{
|
||||
List<BeatmapSetInfo> beatmapSets = null;
|
||||
@ -106,6 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestCurrentRulesetIsRecommended()
|
||||
{
|
||||
BeatmapSetInfo catchSet = null, mixedSet = null;
|
||||
@ -142,6 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestSecondBestRulesetIsRecommended()
|
||||
{
|
||||
BeatmapSetInfo osuSet = null, mixedSet = null;
|
||||
@ -159,6 +161,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestCorrectStarRatingIsUsed()
|
||||
{
|
||||
BeatmapSetInfo osuSet = null, maniaSet = null;
|
||||
@ -176,6 +179,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestBeatmapListingFilter()
|
||||
{
|
||||
AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko");
|
||||
@ -245,7 +249,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
|
||||
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
|
||||
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1]));
|
||||
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(getImport().Beatmaps[expectedDiff - 1].OnlineID));
|
||||
}
|
||||
|
||||
protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API);
|
||||
|
@ -4,20 +4,52 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Seasonal;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneOsuLogo : OsuTestScene
|
||||
{
|
||||
private OsuLogo? logo;
|
||||
|
||||
private float scale = 1;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
AddSliderStep("scale", 0.1, 2, 1, scale =>
|
||||
{
|
||||
if (logo != null)
|
||||
Child.Scale = new Vector2(this.scale = (float)scale);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("Add logo", () =>
|
||||
{
|
||||
Child = new OsuLogo
|
||||
Child = logo = new OsuLogo
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(scale),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChristmas()
|
||||
{
|
||||
AddStep("Add logo", () =>
|
||||
{
|
||||
Child = logo = new OsuLogoChristmas
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(scale),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
@ -4,9 +4,9 @@
|
||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||
</PropertyGroup>
|
||||
<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="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
@ -33,12 +33,12 @@ namespace osu.Game.Audio
|
||||
/// <summary>
|
||||
/// All valid sample addition constants.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
|
||||
public static readonly string[] ALL_ADDITIONS = [HIT_WHISTLE, HIT_FINISH, HIT_CLAP];
|
||||
|
||||
/// <summary>
|
||||
/// All valid bank constants.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM };
|
||||
public static readonly string[] ALL_BANKS = [BANK_NORMAL, BANK_SOFT, BANK_DRUM];
|
||||
|
||||
/// <summary>
|
||||
/// The name of the sample to load.
|
||||
|
@ -533,6 +533,16 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkPlayed(BeatmapInfo beatmapSetInfo) => Realm.Run(r =>
|
||||
{
|
||||
using var transaction = r.BeginWrite();
|
||||
|
||||
var beatmap = r.Find<BeatmapInfo>(beatmapSetInfo.ID)!;
|
||||
beatmap.LastPlayed = DateTimeOffset.Now;
|
||||
|
||||
transaction.Commit();
|
||||
});
|
||||
|
||||
#region Implementation of ICanAcceptFiles
|
||||
|
||||
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps
|
||||
/// Returns <see langword="true"/> if the character <paramref name="c"/> can be used in <see cref="BeatmapMetadata.Artist"/> and <see cref="BeatmapMetadata.Title"/> fields.
|
||||
/// Characters not matched by this method can be placed in <see cref="BeatmapMetadata.ArtistUnicode"/> and <see cref="BeatmapMetadata.TitleUnicode"/>.
|
||||
/// </summary>
|
||||
public static bool IsRomanised(char c) => c <= 0xFF;
|
||||
public static bool IsRomanised(char c) => char.IsAscii(c) && !char.IsControl(c);
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> if the string <paramref name="str"/> can be used in <see cref="BeatmapMetadata.Artist"/> and <see cref="BeatmapMetadata.Title"/> fields.
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Configuration.Tracking;
|
||||
@ -57,6 +58,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f, 0.01f);
|
||||
|
||||
SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal);
|
||||
SetDefault(OsuSetting.BeatmapListingFeaturedArtistFilter, true);
|
||||
|
||||
SetDefault(OsuSetting.ProfileCoverExpanded, true);
|
||||
|
||||
@ -162,6 +164,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.Version, string.Empty);
|
||||
|
||||
SetDefault(OsuSetting.ShowFirstRunSetup, true);
|
||||
SetDefault(OsuSetting.ShowMobileDisclaimer, RuntimeInfo.IsMobile);
|
||||
|
||||
SetDefault(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg);
|
||||
SetDefault(OsuSetting.ScreenshotCaptureMenuCursor, false);
|
||||
@ -217,6 +220,7 @@ namespace osu.Game.Configuration
|
||||
|
||||
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
|
||||
SetDefault(OsuSetting.AlwaysRequireHoldingForPause, false);
|
||||
SetDefault(OsuSetting.EditorShowStoryboard, true);
|
||||
}
|
||||
|
||||
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
||||
@ -450,5 +454,8 @@ namespace osu.Game.Configuration
|
||||
EditorAdjustExistingObjectsOnTimingChanges,
|
||||
AlwaysRequireHoldingForPause,
|
||||
MultiplayerShowInProgressFilter,
|
||||
BeatmapListingFeaturedArtistFilter,
|
||||
ShowMobileDisclaimer,
|
||||
EditorShowStoryboard,
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
@ -30,6 +29,7 @@ namespace osu.Game.Configuration
|
||||
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
|
||||
SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile);
|
||||
SetDefault<ScoreInfo>(Static.LastLocalUserScore, null);
|
||||
SetDefault<ScoreInfo>(Static.LastAppliedOffsetScore, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -78,11 +78,15 @@ namespace osu.Game.Configuration
|
||||
TouchInputActive,
|
||||
|
||||
/// <summary>
|
||||
/// Contains the local user's last score (can be completed or aborted) after exiting <see cref="Player"/>.
|
||||
/// Will be cleared to <c>null</c> when leaving <see cref="PlayerLoader"/>.
|
||||
/// Stores the local user's last score (can be completed or aborted).
|
||||
/// </summary>
|
||||
LastLocalUserScore,
|
||||
|
||||
/// <summary>
|
||||
/// Stores the local user's last score which was used to apply an offset.
|
||||
/// </summary>
|
||||
LastAppliedOffsetScore,
|
||||
|
||||
/// <summary>
|
||||
/// Whether the intro animation for the daily challenge screen has been played once.
|
||||
/// This is reset when a new challenge is up.
|
||||
|
@ -42,7 +42,10 @@ namespace osu.Game.Database
|
||||
return null;
|
||||
|
||||
using var contentStreamReader = new LineBufferedReader(contentStream);
|
||||
var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader);
|
||||
|
||||
// FIRST_LAZER_VERSION is specified here to avoid flooring object coordinates on decode via `(int)` casts.
|
||||
// we will be making integers out of them lower down, but in a slightly different manner (rounding rather than truncating)
|
||||
var beatmapContent = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION).Decode(contentStreamReader);
|
||||
|
||||
var workingBeatmap = new FlatWorkingBeatmap(beatmapContent);
|
||||
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
|
||||
@ -93,6 +96,12 @@ namespace osu.Game.Database
|
||||
|
||||
hitObject.StartTime = Math.Floor(hitObject.StartTime);
|
||||
|
||||
if (hitObject is IHasXPosition hasXPosition)
|
||||
hasXPosition.X = MathF.Round(hasXPosition.X);
|
||||
|
||||
if (hitObject is IHasYPosition hasYPosition)
|
||||
hasYPosition.Y = MathF.Round(hasYPosition.Y);
|
||||
|
||||
if (hitObject is not IHasPath hasPath) continue;
|
||||
|
||||
// stable's hit object parsing expects the entire slider to use only one type of curve,
|
||||
|
@ -95,8 +95,10 @@ namespace osu.Game.Database
|
||||
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
|
||||
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
|
||||
/// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm.
|
||||
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
|
||||
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
|
||||
/// </summary>
|
||||
private const int schema_version = 44;
|
||||
private const int schema_version = 46;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@ -1205,6 +1207,38 @@ namespace osu.Game.Database
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 45:
|
||||
{
|
||||
// Cycling beat snap divisors no longer requires holding shift (just control).
|
||||
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
|
||||
|
||||
var nextBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCycleNextBeatSnapDivisor);
|
||||
if (nextBeatSnapBinding != null && nextBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.MouseWheelLeft }))
|
||||
migration.NewRealm.Remove(nextBeatSnapBinding);
|
||||
|
||||
var previousBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCyclePreviousBeatSnapDivisor);
|
||||
if (previousBeatSnapBinding != null && previousBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Shift, InputKey.Control, InputKey.MouseWheelRight }))
|
||||
migration.NewRealm.Remove(previousBeatSnapBinding);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 46:
|
||||
{
|
||||
// Stable direction didn't match.
|
||||
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
|
||||
|
||||
var nextBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCycleNextBeatSnapDivisor);
|
||||
if (nextBeatSnapBinding != null && nextBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.MouseWheelDown }))
|
||||
migration.NewRealm.Remove(nextBeatSnapBinding);
|
||||
|
||||
var previousBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCyclePreviousBeatSnapDivisor);
|
||||
if (previousBeatSnapBinding != null && previousBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.MouseWheelUp }))
|
||||
migration.NewRealm.Remove(previousBeatSnapBinding);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
@ -266,7 +266,7 @@ namespace osu.Game.Database
|
||||
/// <para>
|
||||
/// If a write transaction did not modify any objects in this <see cref="IRealmCollection{T}" />, the callback is not invoked at all.
|
||||
/// If an error occurs the callback will be invoked with <c>null</c> for the <c>sender</c> parameter and a non-<c>null</c> <c>error</c>.
|
||||
/// Currently the only errors that can occur are when opening the <see cref="Realm" /> on the background worker thread.
|
||||
/// Currently, the only errors that can occur are when opening the <see cref="Realm" /> on the background worker thread.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// At the time when the block is called, the <see cref="IRealmCollection{T}" /> object will be fully evaluated
|
||||
@ -285,8 +285,8 @@ namespace osu.Game.Database
|
||||
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
|
||||
/// To stop receiving notifications, call <see cref="IDisposable.Dispose" />.
|
||||
/// </returns>
|
||||
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IList{T}, NotificationCallbackDelegate{T})" />
|
||||
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IQueryable{T}, NotificationCallbackDelegate{T})" />
|
||||
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IList{T}, NotificationCallbackDelegate{T},KeyPathsCollection?)" />
|
||||
/// <seealso cref="Realms.CollectionExtensions.SubscribeForNotifications{T}(IQueryable{T}, NotificationCallbackDelegate{T},KeyPathsCollection?)" />
|
||||
#pragma warning restore RS0030
|
||||
public static IDisposable QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
|
||||
where T : RealmObjectBase
|
||||
|
@ -46,7 +46,8 @@ namespace osu.Game.Database
|
||||
}
|
||||
|
||||
public IRealmCollection<T> Freeze() => throw new NotImplementedException();
|
||||
public IDisposable SubscribeForNotifications(NotificationCallbackDelegate<T> callback) => throw new NotImplementedException();
|
||||
public IDisposable SubscribeForNotifications(NotificationCallbackDelegate<T> callback, KeyPathsCollection? keyPathCollection = null) => throw new NotImplementedException();
|
||||
|
||||
public bool IsValid => throw new NotImplementedException();
|
||||
public Realm Realm => throw new NotImplementedException();
|
||||
public ObjectSchema ObjectSchema => throw new NotImplementedException();
|
||||
|
@ -142,10 +142,8 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
|
||||
// Framework automatically converts wheel up/down to left/right when shift is held.
|
||||
// See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38.
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
|
||||
|
@ -28,6 +28,11 @@ This includes content that may not be correctly licensed for osu! usage. Browse
|
||||
/// </summary>
|
||||
public static LocalisableString UserContentConfirmButtonText => new TranslatableString(getKey(@"understood"), @"I understand");
|
||||
|
||||
/// <summary>
|
||||
/// "Featured Artists are music artists who have collaborated with osu! to make a selection of their tracks available for use in beatmaps. For some osu! releases, we showcase only featured artist beatmaps to better support the surrounding ecosystem."
|
||||
/// </summary>
|
||||
public static LocalisableString FeaturedArtistsTooltip => new TranslatableString(getKey(@"featured_artists_disabled_tooltip"), @"Featured Artists are music artists who have collaborated with osu! to make a selection of their tracks available for use in beatmaps. For some osu! releases, we showcase only featured artist beatmaps to better support the surrounding ecosystem.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,25 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge");
|
||||
|
||||
/// <summary>
|
||||
/// "A few important words from your dev team!"
|
||||
/// </summary>
|
||||
public static LocalisableString MobileDisclaimerHeader => new TranslatableString(getKey(@"mobile_disclaimer_header"), @"A few important words from your dev team!");
|
||||
|
||||
/// <summary>
|
||||
/// "While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version.
|
||||
///
|
||||
/// Your experience will not be perfect, and may even feel subpar compared to games which are made mobile-first.
|
||||
///
|
||||
/// Please bear with us as we continue to improve the game for you!"
|
||||
/// </summary>
|
||||
public static LocalisableString MobileDisclaimerBody => new TranslatableString(getKey(@"mobile_disclaimer_body"),
|
||||
@"While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version.
|
||||
|
||||
Your experience will not be perfect, and may even feel subpar compared to games which are made mobile-first.
|
||||
|
||||
Please bear with us as we continue to improve the game for you!");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -15,10 +15,9 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
|
||||
|
||||
/// <summary>
|
||||
/// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way."
|
||||
/// "If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way."
|
||||
/// </summary>
|
||||
public static LocalisableString Description => new TranslatableString(getKey(@"description"),
|
||||
@"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way.");
|
||||
public static LocalisableString Description => new TranslatableString(getKey(@"description"), @"If you have an installation of a previous osu! version, you can choose to migrate your existing content. Note that this will not affect your existing installation's files in any way.");
|
||||
|
||||
/// <summary>
|
||||
/// "previous osu! install"
|
||||
@ -38,8 +37,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Your import will continue in the background. Check on its progress in the notifications sidebar!"
|
||||
/// </summary>
|
||||
public static LocalisableString ImportInProgress =>
|
||||
new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!");
|
||||
public static LocalisableString ImportInProgress => new TranslatableString(getKey(@"import_in_progress"), @"Your import will continue in the background. Check on its progress in the notifications sidebar!");
|
||||
|
||||
/// <summary>
|
||||
/// "calculating..."
|
||||
|
@ -84,12 +84,12 @@ Please try changing your audio device to a working setting.");
|
||||
public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!");
|
||||
|
||||
/// <summary>
|
||||
/// "You received a private message from '{0}'. Click to read it!"
|
||||
/// "You received a private message from '{0}'. Click to read it!"
|
||||
/// </summary>
|
||||
public static LocalisableString PrivateMessageReceived(string username) => new TranslatableString(getKey(@"private_message_received"), @"You received a private message from '{0}'. Click to read it!", username);
|
||||
|
||||
/// <summary>
|
||||
/// "Your name was mentioned in chat by '{0}'. Click to find out why!"
|
||||
/// "Your name was mentioned in chat by '{0}'. Click to find out why!"
|
||||
/// </summary>
|
||||
public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username);
|
||||
|
||||
@ -115,7 +115,7 @@ Please try changing your audio device to a working setting.");
|
||||
|
||||
/// <summary>
|
||||
/// "You are now running osu! {0}.
|
||||
/// Click to see what's new!"
|
||||
/// Click to see what's new!"
|
||||
/// </summary>
|
||||
public static LocalisableString GameVersionAfterUpdate(string version) => new TranslatableString(getKey(@"game_version_after_update"), @"You are now running osu! {0}.
|
||||
Click to see what's new!", version);
|
||||
|
@ -54,16 +54,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString BeatmapHitsounds => new TranslatableString(getKey(@"beatmap_hitsounds"), @"Beatmap hitsounds");
|
||||
|
||||
/// <summary>
|
||||
/// "Export selected skin"
|
||||
/// </summary>
|
||||
public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin");
|
||||
|
||||
/// <summary>
|
||||
/// "Delete selected skin"
|
||||
/// </summary>
|
||||
public static LocalisableString DeleteSkinButton => new TranslatableString(getKey(@"delete_skin_button"), @"Delete selected skin");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -10,10 +10,12 @@ using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
{
|
||||
public class ModSettingsDictionaryFormatter : IMessagePackFormatter<Dictionary<string, object>>
|
||||
public class ModSettingsDictionaryFormatter : IMessagePackFormatter<Dictionary<string, object>?>
|
||||
{
|
||||
public void Serialize(ref MessagePackWriter writer, Dictionary<string, object> value, MessagePackSerializerOptions options)
|
||||
public void Serialize(ref MessagePackWriter writer, Dictionary<string, object>? value, MessagePackSerializerOptions options)
|
||||
{
|
||||
if (value == null) return;
|
||||
|
||||
var primitiveFormatter = PrimitiveObjectFormatter.Instance;
|
||||
|
||||
writer.WriteArrayHeader(value.Count);
|
||||
|
@ -4,13 +4,16 @@
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
@ -23,9 +26,15 @@ namespace osu.Game.Online.Chat
|
||||
[Resolved]
|
||||
private Clipboard clipboard { get; set; } = null!;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private INotificationOverlay? notificationOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private Bindable<bool> externalLinkWarning = null!;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
@ -34,9 +43,51 @@ namespace osu.Game.Online.Chat
|
||||
externalLinkWarning = config.GetBindable<bool>(OsuSetting.ExternalLinkWarning);
|
||||
}
|
||||
|
||||
public void OpenUrlExternally(string url, bool bypassWarning = false)
|
||||
public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default)
|
||||
{
|
||||
if (!bypassWarning && externalLinkWarning.Value && dialogOverlay != null)
|
||||
bool isTrustedDomain;
|
||||
|
||||
if (url.StartsWith('/'))
|
||||
{
|
||||
url = $"{api.WebsiteRootUrl}{url}";
|
||||
isTrustedDomain = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isTrustedDomain = url.StartsWith(api.WebsiteRootUrl, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (!url.CheckIsValidUrl())
|
||||
{
|
||||
notificationOverlay?.Post(new SimpleErrorNotification
|
||||
{
|
||||
Text = NotificationsStrings.UnsupportedOrDangerousUrlProtocol(url),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
bool shouldWarn;
|
||||
|
||||
switch (warnMode)
|
||||
{
|
||||
case LinkWarnMode.Default:
|
||||
shouldWarn = externalLinkWarning.Value && !isTrustedDomain;
|
||||
break;
|
||||
|
||||
case LinkWarnMode.AlwaysWarn:
|
||||
shouldWarn = true;
|
||||
break;
|
||||
|
||||
case LinkWarnMode.NeverWarn:
|
||||
shouldWarn = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(warnMode), warnMode, null);
|
||||
}
|
||||
|
||||
if (dialogOverlay != null && shouldWarn)
|
||||
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url)));
|
||||
else
|
||||
host.OpenUrlExternally(url);
|
||||
|
23
osu.Game/Online/Chat/LinkWarnMode.cs
Normal file
23
osu.Game/Online/Chat/LinkWarnMode.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
public enum LinkWarnMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Will show a dialog when opening a URL that is not on a trusted domain.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Will always show a dialog when opening a URL.
|
||||
/// </summary>
|
||||
AlwaysWarn,
|
||||
|
||||
/// <summary>
|
||||
/// Will never show a dialog when opening a URL.
|
||||
/// </summary>
|
||||
NeverWarn,
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using MessagePack;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
|
||||
{
|
||||
[MessagePackObject]
|
||||
public class TeamVersusUserState : MatchUserState
|
||||
{
|
||||
[Key(0)]
|
||||
|
@ -18,7 +18,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -68,6 +67,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Seasonal;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Users;
|
||||
@ -220,6 +220,11 @@ namespace osu.Game
|
||||
|
||||
private readonly List<OverlayContainer> visibleBlockingOverlays = new List<OverlayContainer>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the game should be limited to only display officially licensed content.
|
||||
/// </summary>
|
||||
public virtual bool HideUnlicensedContent => false;
|
||||
|
||||
public OsuGame(string[] args = null)
|
||||
{
|
||||
this.args = args;
|
||||
@ -319,6 +324,7 @@ namespace osu.Game
|
||||
|
||||
if (host.Window != null)
|
||||
{
|
||||
host.Window.CursorState |= CursorState.Hidden;
|
||||
host.Window.DragDrop += path =>
|
||||
{
|
||||
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
|
||||
@ -361,7 +367,10 @@ namespace osu.Game
|
||||
{
|
||||
SentryLogger.AttachUser(API.LocalUser);
|
||||
|
||||
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
|
||||
if (SeasonalUIConfig.ENABLED)
|
||||
dependencies.CacheAs(osuLogo = new OsuLogoChristmas { Alpha = 0 });
|
||||
else
|
||||
dependencies.CacheAs(osuLogo = new OsuLogo { Alpha = 0 });
|
||||
|
||||
// bind config int to database RulesetInfo
|
||||
configRuleset = LocalConfig.GetBindable<string>(OsuSetting.Ruleset);
|
||||
@ -506,32 +515,7 @@ namespace osu.Game
|
||||
onScreenDisplay.Display(new CopyUrlToast());
|
||||
});
|
||||
|
||||
public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ =>
|
||||
{
|
||||
bool isTrustedDomain;
|
||||
|
||||
if (url.StartsWith('/'))
|
||||
{
|
||||
url = $"{API.WebsiteRootUrl}{url}";
|
||||
isTrustedDomain = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isTrustedDomain = url.StartsWith(API.WebsiteRootUrl, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (!url.CheckIsValidUrl())
|
||||
{
|
||||
Notifications.Post(new SimpleErrorNotification
|
||||
{
|
||||
Text = NotificationsStrings.UnsupportedOrDangerousUrlProtocol(url),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
externalLinkOpener.OpenUrlExternally(url, forceBypassExternalUrlWarning || isTrustedDomain);
|
||||
});
|
||||
public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) => waitForReady(() => externalLinkOpener, _ => externalLinkOpener.OpenUrlExternally(url, warnMode));
|
||||
|
||||
/// <summary>
|
||||
/// Open a specific channel in chat.
|
||||
@ -1330,7 +1314,7 @@ namespace osu.Game
|
||||
IconColour = Colours.YellowDark,
|
||||
Activated = () =>
|
||||
{
|
||||
OpenUrlExternally("https://opentabletdriver.net/Tablets", true);
|
||||
OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
@ -1418,24 +1402,25 @@ namespace osu.Game
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
if (introScreen == null) return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.DecreaseVolume:
|
||||
case GlobalAction.IncreaseVolume:
|
||||
return volume.Adjust(e.Action);
|
||||
}
|
||||
|
||||
// All actions below this point don't allow key repeat.
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
// Wait until we're loaded at least to the intro before allowing various interactions.
|
||||
if (introScreen == null) return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.ToggleMute:
|
||||
case GlobalAction.NextVolumeMeter:
|
||||
case GlobalAction.PreviousVolumeMeter:
|
||||
|
||||
if (e.Repeat)
|
||||
return true;
|
||||
|
||||
return volume.Adjust(e.Action);
|
||||
|
||||
case GlobalAction.ToggleFPSDisplay:
|
||||
|
@ -315,6 +315,7 @@ namespace osu.Game
|
||||
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, API, LocalConfig));
|
||||
|
||||
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
|
||||
dependencies.CacheAs<IWorkingBeatmapCache>(BeatmapManager);
|
||||
|
||||
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
|
||||
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
|
||||
|
@ -18,6 +18,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
@ -213,7 +214,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
if (!string.IsNullOrEmpty(errors.Message))
|
||||
passwordDescription.AddErrors(new[] { errors.Message });
|
||||
|
||||
game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
|
||||
game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", LinkWarnMode.NeverWarn);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
@ -113,7 +114,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
}
|
||||
}
|
||||
|
||||
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
|
||||
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem, IHasTooltip
|
||||
{
|
||||
private Bindable<bool> disclaimerShown = null!;
|
||||
|
||||
@ -125,17 +126,36 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SessionStatics sessionStatics { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
public LocalisableString TooltipText => BeatmapOverlayStrings.FeaturedArtistsTooltip;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
config.BindWith(OsuSetting.BeatmapListingFeaturedArtistFilter, Active);
|
||||
disclaimerShown = sessionStatics.GetBindable<bool>(Static.FeaturedArtistDisclaimerShownOnce);
|
||||
|
||||
// no need to show the disclaimer if the user already had it toggled off in config.
|
||||
if (!Active.Value)
|
||||
disclaimerShown.Value = true;
|
||||
|
||||
if (game?.HideUnlicensedContent == true)
|
||||
{
|
||||
Enabled.Value = false;
|
||||
Active.Disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Color4 ColourNormal => colours.Orange1;
|
||||
@ -143,6 +163,9 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (!Enabled.Value)
|
||||
return true;
|
||||
|
||||
if (!disclaimerShown.Value && dialogOverlay != null)
|
||||
{
|
||||
dialogOverlay.Push(new FeaturedArtistConfirmDialog(() =>
|
||||
|
@ -73,7 +73,10 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
private void currentChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
foreach (var c in Children)
|
||||
c.Active.Value = Current.Contains(c.Value);
|
||||
{
|
||||
if (!c.Active.Disabled)
|
||||
c.Active.Value = Current.Contains(c.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -100,7 +103,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
protected partial class MultipleSelectionFilterTabItem : FilterTabItem<T>
|
||||
{
|
||||
private Drawable activeContent = null!;
|
||||
private Container activeContent = null!;
|
||||
private Circle background = null!;
|
||||
|
||||
public MultipleSelectionFilterTabItem(T value)
|
||||
@ -160,7 +163,9 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
Color4 colour = Active.Value ? ColourActive : ColourNormal;
|
||||
|
||||
if (IsHovered)
|
||||
if (!Enabled.Value)
|
||||
colour = colour.Darken(1f);
|
||||
else if (IsHovered)
|
||||
colour = Active.Value ? colour.Darken(0.2f) : colour.Lighten(0.2f);
|
||||
|
||||
if (Active.Value)
|
||||
|
@ -57,7 +57,9 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Enabled.BindValueChanged(_ => UpdateState());
|
||||
UpdateState();
|
||||
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,9 @@ namespace osu.Game.Overlays.Dialog
|
||||
return;
|
||||
|
||||
bodyText = value;
|
||||
|
||||
body.Text = value;
|
||||
body.TextAnchor = bodyText.ToString().Contains('\n') ? Anchor.TopLeft : Anchor.TopCentre;
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,13 +212,12 @@ namespace osu.Game.Overlays.Dialog
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
Padding = new MarginPadding { Horizontal = 15 },
|
||||
Padding = new MarginPadding { Horizontal = 15, Bottom = 10 },
|
||||
},
|
||||
body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18))
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
TextAnchor = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = 15 },
|
||||
@ -301,6 +302,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
content.ScaleTo(0.7f);
|
||||
ring.ResizeTo(ringMinifiedSize);
|
||||
icon.ScaleTo(0f);
|
||||
}
|
||||
|
||||
content
|
||||
@ -308,6 +310,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
.FadeIn(ENTER_DURATION, Easing.OutQuint);
|
||||
|
||||
ring.ResizeTo(ringSize, ENTER_DURATION * 1.5f, Easing.OutQuint);
|
||||
icon.Delay(100).ScaleTo(1, ENTER_DURATION * 1.5f, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
@ -121,9 +121,11 @@ namespace osu.Game.Overlays.Login
|
||||
|
||||
codeTextBox.Current.BindValueChanged(code =>
|
||||
{
|
||||
if (code.NewValue.Length == 8)
|
||||
string trimmedCode = code.NewValue.Trim();
|
||||
|
||||
if (trimmedCode.Length == 8)
|
||||
{
|
||||
api.AuthenticateSecondFactor(code.NewValue);
|
||||
api.AuthenticateSecondFactor(trimmedCode);
|
||||
codeTextBox.Current.Disabled = true;
|
||||
}
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
@ -87,7 +88,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
background.Colour = colours.Pink;
|
||||
|
||||
Action = () => game?.OpenUrlExternally(@"/home/support");
|
||||
// Easy to accidentally click so let's always show the open URL popup.
|
||||
Action = () => game?.OpenUrlExternally(@"/home/support", LinkWarnMode.AlwaysWarn);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
@ -9,17 +9,23 @@ using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections
|
||||
@ -64,13 +70,26 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
Current = skins.CurrentSkinInfo,
|
||||
Keywords = new[] { @"skins" },
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5, 0),
|
||||
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// This is all super-temporary until we move skin settings to their own panel / overlay.
|
||||
new RenameSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 120 },
|
||||
new ExportSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 120 },
|
||||
new DeleteSkinButton { Padding = new MarginPadding(), RelativeSizeAxes = Axes.None, Width = 110 },
|
||||
}
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = SkinSettingsStrings.SkinLayoutEditor,
|
||||
Action = () => skinEditor?.ToggleVisibility(),
|
||||
},
|
||||
new ExportSkinButton(),
|
||||
new DeleteSkinButton(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -136,6 +155,34 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
}
|
||||
}
|
||||
|
||||
public partial class RenameSkinButton : SettingsButton, IHasPopover
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
private Bindable<Skin> currentSkin;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Text = "Rename";
|
||||
Action = this.ShowPopover;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
currentSkin = skins.CurrentSkin.GetBoundCopy();
|
||||
currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true);
|
||||
}
|
||||
|
||||
public Popover GetPopover()
|
||||
{
|
||||
return new RenameSkinPopover();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ExportSkinButton : SettingsButton
|
||||
{
|
||||
[Resolved]
|
||||
@ -146,7 +193,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Text = SkinSettingsStrings.ExportSkinButton;
|
||||
Text = "Export";
|
||||
Action = export;
|
||||
}
|
||||
|
||||
@ -184,7 +231,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Text = SkinSettingsStrings.DeleteSkinButton;
|
||||
Text = "Delete";
|
||||
Action = delete;
|
||||
}
|
||||
|
||||
@ -201,5 +248,63 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value));
|
||||
}
|
||||
}
|
||||
|
||||
public partial class RenameSkinPopover : OsuPopover
|
||||
{
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; }
|
||||
|
||||
private readonly FocusedTextBox textBox;
|
||||
|
||||
public RenameSkinPopover()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
RoundedButton renameButton;
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 250,
|
||||
Spacing = new Vector2(10f),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
textBox = new FocusedTextBox
|
||||
{
|
||||
PlaceholderText = @"Skin name",
|
||||
FontSize = OsuFont.DEFAULT_FONT_SIZE,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
SelectAllOnFocus = true,
|
||||
},
|
||||
renameButton = new RoundedButton
|
||||
{
|
||||
Height = 40,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
MatchingFilter = true,
|
||||
Text = "Save",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
renameButton.Action += rename;
|
||||
textBox.OnCommit += (_, _) => rename();
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
textBox.Text = skins.CurrentSkinInfo.Value.Value.Name;
|
||||
textBox.TakeFocus();
|
||||
|
||||
base.PopIn();
|
||||
}
|
||||
|
||||
private void rename() => skins.CurrentSkinInfo.Value.PerformWrite(skin =>
|
||||
{
|
||||
skin.Name = textBox.Text;
|
||||
PopOut();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,11 +36,13 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
Keywords = new[] { "intro", "welcome" },
|
||||
LabelText = UserInterfaceStrings.InterfaceVoices,
|
||||
Current = config.GetBindable<bool>(OsuSetting.MenuVoice)
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
Keywords = new[] { "intro", "welcome" },
|
||||
LabelText = UserInterfaceStrings.OsuMusicTheme,
|
||||
Current = config.GetBindable<bool>(OsuSetting.MenuMusic)
|
||||
},
|
||||
|
@ -374,9 +374,10 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
return;
|
||||
}
|
||||
|
||||
changeHandler = new SkinEditorChangeHandler(skinComponentsContainer);
|
||||
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
if (skinComponentsContainer.IsLoaded)
|
||||
bindChangeHandler(skinComponentsContainer);
|
||||
else
|
||||
skinComponentsContainer.OnLoadComplete += d => Schedule(() => bindChangeHandler((SkinnableContainer)d));
|
||||
|
||||
content.Child = new SkinBlueprintContainer(skinComponentsContainer);
|
||||
|
||||
@ -418,10 +419,21 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
SelectedComponents.Clear();
|
||||
placeComponent(component);
|
||||
}
|
||||
|
||||
void bindChangeHandler(SkinnableContainer skinnableContainer)
|
||||
{
|
||||
changeHandler = new SkinEditorChangeHandler(skinnableContainer);
|
||||
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void skinChanged()
|
||||
{
|
||||
if (skins.EnsureMutableSkin())
|
||||
// Another skin changed event will arrive which will complete the process.
|
||||
return;
|
||||
|
||||
headerText.Clear();
|
||||
|
||||
headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16));
|
||||
@ -439,17 +451,24 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
});
|
||||
|
||||
changeHandler?.Dispose();
|
||||
changeHandler = null;
|
||||
|
||||
skins.EnsureMutableSkin();
|
||||
// Schedule is required to ensure that all layout in `LoadComplete` methods has been completed
|
||||
// before storing an undo state.
|
||||
//
|
||||
// See https://github.com/ppy/osu/blob/8e6a4559e3ae8c9892866cf9cf8d4e8d1b72afd0/osu.Game/Skinning/SkinReloadableDrawable.cs#L76.
|
||||
Schedule(() =>
|
||||
{
|
||||
var targetContainer = getTarget(selectedTarget.Value);
|
||||
|
||||
var targetContainer = getTarget(selectedTarget.Value);
|
||||
if (targetContainer != null)
|
||||
changeHandler = new SkinEditorChangeHandler(targetContainer);
|
||||
|
||||
if (targetContainer != null)
|
||||
changeHandler = new SkinEditorChangeHandler(targetContainer);
|
||||
hasBegunMutating = true;
|
||||
hasBegunMutating = true;
|
||||
|
||||
// Reload sidebar components.
|
||||
selectedTarget.TriggerChange();
|
||||
// Reload sidebar components.
|
||||
selectedTarget.TriggerChange();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
return;
|
||||
|
||||
components = new BindableList<ISerialisableDrawable> { BindTarget = firstTarget.Components };
|
||||
components.BindCollectionChanged((_, _) => SaveState());
|
||||
components.BindCollectionChanged((_, _) => SaveState(), true);
|
||||
}
|
||||
|
||||
protected override void WriteCurrentStateToStream(MemoryStream stream)
|
||||
|
@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
string bank = parts[0];
|
||||
string sampleSet = parts[1];
|
||||
|
||||
return HitSampleInfo.AllBanks.Contains(bank)
|
||||
&& HitSampleInfo.AllAdditions.Append(HitSampleInfo.HIT_NORMAL).Any(sampleSet.StartsWith);
|
||||
return HitSampleInfo.ALL_BANKS.Contains(bank)
|
||||
&& HitSampleInfo.ALL_ADDITIONS.Append(HitSampleInfo.HIT_NORMAL).Any(sampleSet.StartsWith);
|
||||
}
|
||||
|
||||
public class IssueTemplateConsequentDelay : IssueTemplate
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
++objectsWithoutHitsounds;
|
||||
}
|
||||
|
||||
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains);
|
||||
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.ALL_ADDITIONS.Any(sample.Name.Contains);
|
||||
private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
public abstract class IssueTemplateLongPeriod : IssueTemplate
|
||||
|
@ -191,9 +191,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<TernaryButton> CreateTernaryButtons() => new[]
|
||||
public IEnumerable<DrawableTernaryButton> CreateTernaryButtons() => new[]
|
||||
{
|
||||
new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap })
|
||||
new DrawableTernaryButton
|
||||
{
|
||||
Current = DistanceSnapToggle,
|
||||
Description = "Distance Snap",
|
||||
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap },
|
||||
}
|
||||
};
|
||||
|
||||
public void HandleToggleViaKey(KeyboardEvent key)
|
||||
|
@ -55,12 +55,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos);
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos);
|
||||
|
||||
private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.ScreenSpaceDrawQuad.Contains(screenSpacePos);
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
|
@ -269,10 +269,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
};
|
||||
}
|
||||
|
||||
TernaryStates = CreateTernaryButtons().ToArray();
|
||||
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
|
||||
togglesCollection.AddRange(CreateTernaryButtons().ToArray());
|
||||
|
||||
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Zip(BlueprintContainer.SampleAdditionBankTernaryStates).Select(b => new SampleBankTernaryButton(b.First, b.Second)));
|
||||
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates);
|
||||
|
||||
SetSelectTool();
|
||||
|
||||
@ -368,15 +367,10 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// </remarks>
|
||||
protected abstract IReadOnlyList<CompositionTool> CompositionTools { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A collection of states which will be displayed to the user in the toolbox.
|
||||
/// </summary>
|
||||
public TernaryButton[] TernaryStates { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create all ternary states required to be displayed to the user.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
|
||||
protected virtual IEnumerable<DrawableTernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
|
||||
@ -437,7 +431,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button)
|
||||
{
|
||||
button.Button.Toggle();
|
||||
button.Toggle();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,12 @@ namespace osu.Game.Rulesets.Edit
|
||||
Spacing = new Vector2(0, 5),
|
||||
Children = new[]
|
||||
{
|
||||
new DrawableTernaryButton(new TernaryButton(showSpeedChanges, "Show speed changes", () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt }))
|
||||
new DrawableTernaryButton
|
||||
{
|
||||
Current = showSpeedChanges,
|
||||
Description = "Show speed changes",
|
||||
CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.TachometerAlt },
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -21,9 +21,17 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
|
||||
public int ComboOffset { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Position.Y);
|
||||
}
|
||||
|
||||
public float Y => Position.Y;
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(Position.X, value);
|
||||
}
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
|
@ -44,13 +44,13 @@ namespace osu.Game.Rulesets.Objects
|
||||
PathProgress = 0,
|
||||
};
|
||||
|
||||
if (tickDistance != 0)
|
||||
for (int span = 0; span < spanCount; span++)
|
||||
{
|
||||
for (int span = 0; span < spanCount; span++)
|
||||
{
|
||||
double spanStartTime = startTime + span * spanDuration;
|
||||
bool reversed = span % 2 == 1;
|
||||
double spanStartTime = startTime + span * spanDuration;
|
||||
bool reversed = span % 2 == 1;
|
||||
|
||||
if (tickDistance != 0)
|
||||
{
|
||||
var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken);
|
||||
|
||||
if (reversed)
|
||||
@ -61,18 +61,18 @@ namespace osu.Game.Rulesets.Objects
|
||||
|
||||
foreach (var e in ticks)
|
||||
yield return e;
|
||||
}
|
||||
|
||||
if (span < spanCount - 1)
|
||||
if (span < spanCount - 1)
|
||||
{
|
||||
yield return new SliderEventDescriptor
|
||||
{
|
||||
yield return new SliderEventDescriptor
|
||||
{
|
||||
Type = SliderEventType.Repeat,
|
||||
SpanIndex = span,
|
||||
SpanStartTime = startTime + span * spanDuration,
|
||||
Time = spanStartTime + spanDuration,
|
||||
PathProgress = (span + 1) % 2,
|
||||
};
|
||||
}
|
||||
Type = SliderEventType.Repeat,
|
||||
SpanIndex = span,
|
||||
SpanStartTime = startTime + span * spanDuration,
|
||||
Time = spanStartTime + spanDuration,
|
||||
PathProgress = (span + 1) % 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// The starting position of the HitObject.
|
||||
/// </summary>
|
||||
Vector2 Position { get; }
|
||||
Vector2 Position { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// The starting X-position of this HitObject.
|
||||
/// </summary>
|
||||
float X { get; }
|
||||
float X { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// The starting Y-position of this HitObject.
|
||||
/// </summary>
|
||||
float Y { get; }
|
||||
float Y { get; set; }
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user