diff --git a/CodeAnalysis/osu.globalconfig b/CodeAnalysis/osu.globalconfig
index 247a825033..8012c31eca 100644
--- a/CodeAnalysis/osu.globalconfig
+++ b/CodeAnalysis/osu.globalconfig
@@ -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
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index f77cda1533..1d368e9bd1 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 47cabaddb1..d69bc78b8f 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index a7d62291d0..7ac269f65f 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 47cabaddb1..d69bc78b8f 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,9 +9,9 @@
false
-
+
-
+
diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs
index 6f53c65ca9..4a5fc6218e 100644
--- a/osu.Desktop/Windows/WindowsAssociationManager.cs
+++ b/osu.Desktop/Windows/WindowsAssociationManager.cs
@@ -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";
///
/// 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.
///
- 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.
///
///
- /// Call in a timely fashion to keep descriptions up-to-date and localised.
+ /// Call in a timely fashion to keep descriptions up-to-date and localised.
///
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.
///
///
- /// Call in a timely fashion to keep descriptions up-to-date and localised.
+ /// Call in a timely fashion to keep descriptions up-to-date and localised.
///
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
///
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;
+ }
+
+ ///
+ /// Registers an application capability according to
+ /// Registering an Application for Use with Default Programs.
+ ///
+ 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;
+ }
///
/// 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));
}
///
@@ -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
{
///
/// "The URL Protocol 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).
///
- 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}";
///
/// 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);
}
}
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index d06c4dd41b..21c570a7b2 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,9 +24,9 @@
-
+
-
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 9764c71493..8a353eb2f5 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
index 7b665b1ff9..278c7b1bde 100644
--- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . 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()
{
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index b434d6aaf9..56ee208670 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
index e626392234..6a671458f0 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs
@@ -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;
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
index b2ee43ba16..26b26641d3 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/SelectionEditablePath.cs
@@ -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);
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
index 7b57dac36e..21cc260462 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
@@ -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)
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index aae3369d40..e0d80e0e64 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
- protected override IEnumerable CreateTernaryButtons()
+ protected override IEnumerable CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons());
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index e7abd47881..5e4bad279b 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 5ea231e606..267dc98985 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -1,10 +1,10 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 3504954bec..740862c9fd 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -626,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)
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 7c50558b92..e8b9d0544e 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -53,9 +53,14 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
- protected override IEnumerable CreateTernaryButtons()
+ protected override IEnumerable 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 selectedHitObjects;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 0fcfdef4ee..e22e1d2001 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -382,6 +382,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
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()
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
index 87b89a07cf..1fbdbafec4 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
@@ -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);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs
index ad49150d81..5e2d04700d 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultReverseArrow.cs
@@ -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));
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
index ad1fb98aef..85c895006b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
@@ -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.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;
}
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 2170009ae8..523df4c259 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -1,9 +1,9 @@
-
+
-
+
WinExe
diff --git a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs
index bbcf6aac2c..c625346645 100644
--- a/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs
+++ b/osu.Game.Tests/Editing/TestSceneEditorBeatmapProcessor.cs
@@ -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));
+ });
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index 765ffb4549..f65a3e67e8 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -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 editorDim;
-
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
- {
- editorDim = config.GetBindable(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().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo));
- return background.DimWhenUserSettingsIgnored.Value == editorDim.Value && background.BlurAmount.Value == 0;
- });
+ AddUntilStep("background is correct", () => this.ChildrenOfType().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().Single(b => ReferenceEquals(b.Beatmap.BeatmapInfo, EditorBeatmap.BeatmapInfo));
- return background.DimWhenUserSettingsIgnored.Value == editorDim.Value && background.BlurAmount.Value == 0;
- });
+ AddUntilStep("background is correct", () => this.ChildrenOfType().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().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().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)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index a7ab021884..4ad6bc66e3 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -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().First().Position == recordingManager.ChildrenOfType().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()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs
index 88afef7de2..ecdbfc411a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneStarRatingRangeDisplay.cs
@@ -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() },
];
});
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index aa452101bf..5c89e8a02c 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -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 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);
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 01d2241650..e78a3ea4f3 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -1,11 +1,11 @@
-
+
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 04683cd83b..1daf5a446e 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -4,9 +4,9 @@
osu.Game.Tournament.Tests.TournamentTestRunner
-
+
-
+
WinExe
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index dd3abb6f81..6f32e1e7fb 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -220,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)
@@ -455,5 +456,6 @@ namespace osu.Game.Configuration
MultiplayerShowInProgressFilter,
BeatmapListingFeaturedArtistFilter,
ShowMobileDisclaimer,
+ EditorShowStoryboard,
}
}
diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs
index df725505fc..538ac1dff7 100644
--- a/osu.Game/Database/RealmObjectExtensions.cs
+++ b/osu.Game/Database/RealmObjectExtensions.cs
@@ -266,7 +266,7 @@ namespace osu.Game.Database
///
/// If a write transaction did not modify any objects in this , the callback is not invoked at all.
/// If an error occurs the callback will be invoked with null for the sender parameter and a non-null error.
- /// Currently the only errors that can occur are when opening the on the background worker thread.
+ /// Currently, the only errors that can occur are when opening the on the background worker thread.
///
///
/// At the time when the block is called, the 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 .
///
- ///
- ///
+ ///
+ ///
#pragma warning restore RS0030
public static IDisposable QueryAsyncWithNotifications(this IRealmCollection collection, NotificationCallbackDelegate callback)
where T : RealmObjectBase
diff --git a/osu.Game/Database/RealmResetEmptySet.cs b/osu.Game/Database/RealmResetEmptySet.cs
index 9f9a1ba6d7..0daedc9633 100644
--- a/osu.Game/Database/RealmResetEmptySet.cs
+++ b/osu.Game/Database/RealmResetEmptySet.cs
@@ -46,7 +46,8 @@ namespace osu.Game.Database
}
public IRealmCollection Freeze() => throw new NotImplementedException();
- public IDisposable SubscribeForNotifications(NotificationCallbackDelegate callback) => throw new NotImplementedException();
+ public IDisposable SubscribeForNotifications(NotificationCallbackDelegate 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();
diff --git a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
index 521a77fe20..6293a4f840 100644
--- a/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
+++ b/osu.Game/Localisation/FirstRunOverlayImportFromStableScreenStrings.cs
@@ -15,10 +15,9 @@ namespace osu.Game.Localisation
public static LocalisableString Header => new TranslatableString(getKey(@"header"), @"Import");
///
- /// "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."
///
- 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.");
///
/// "previous osu! install"
@@ -38,8 +37,7 @@ namespace osu.Game.Localisation
///
/// "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!");
+ 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!");
///
/// "calculating..."
diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs
index d8f768f2d8..bb2990f782 100644
--- a/osu.Game/Localisation/NotificationsStrings.cs
+++ b/osu.Game/Localisation/NotificationsStrings.cs
@@ -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!");
///
- /// "You received a private message from '{0}'. Click to read it!"
+ /// "You received a private message from '{0}'. Click to read it!"
///
public static LocalisableString PrivateMessageReceived(string username) => new TranslatableString(getKey(@"private_message_received"), @"You received a private message from '{0}'. Click to read it!", username);
///
- /// "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!"
///
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.");
///
/// "You are now running osu! {0}.
- /// Click to see what's new!"
+ /// Click to see what's new!"
///
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);
diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
index 3fad032531..8da83d2aad 100644
--- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
+++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs
@@ -10,10 +10,12 @@ using osu.Game.Configuration;
namespace osu.Game.Online.API
{
- public class ModSettingsDictionaryFormatter : IMessagePackFormatter>
+ public class ModSettingsDictionaryFormatter : IMessagePackFormatter?>
{
- public void Serialize(ref MessagePackWriter writer, Dictionary value, MessagePackSerializerOptions options)
+ public void Serialize(ref MessagePackWriter writer, Dictionary? value, MessagePackSerializerOptions options)
{
+ if (value == null) return;
+
var primitiveFormatter = PrimitiveObjectFormatter.Instance;
writer.WriteArrayHeader(value.Count);
diff --git a/osu.Game/Online/Chat/ExternalLinkOpener.cs b/osu.Game/Online/Chat/ExternalLinkOpener.cs
index 75b161d57b..f76d42c96d 100644
--- a/osu.Game/Online/Chat/ExternalLinkOpener.cs
+++ b/osu.Game/Online/Chat/ExternalLinkOpener.cs
@@ -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 externalLinkWarning = null!;
[BackgroundDependencyLoader(true)]
@@ -34,9 +43,51 @@ namespace osu.Game.Online.Chat
externalLinkWarning = config.GetBindable(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);
diff --git a/osu.Game/Online/Chat/LinkWarnMode.cs b/osu.Game/Online/Chat/LinkWarnMode.cs
new file mode 100644
index 0000000000..0acd3994d8
--- /dev/null
+++ b/osu.Game/Online/Chat/LinkWarnMode.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . 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
+ {
+ ///
+ /// Will show a dialog when opening a URL that is not on a trusted domain.
+ ///
+ Default,
+
+ ///
+ /// Will always show a dialog when opening a URL.
+ ///
+ AlwaysWarn,
+
+ ///
+ /// Will never show a dialog when opening a URL.
+ ///
+ NeverWarn,
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs
index ac3b9724cc..bf11713663 100644
--- a/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs
+++ b/osu.Game/Online/Multiplayer/MatchTypes/TeamVersus/TeamVersusUserState.cs
@@ -5,6 +5,7 @@ using MessagePack;
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
{
+ [MessagePackObject]
public class TeamVersusUserState : MatchUserState
{
[Key(0)]
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index c20536a1ec..0d86bdecde 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -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;
@@ -516,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));
///
/// Open a specific channel in chat.
@@ -1340,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;
}
}));
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index fb6a5796a1..b2b672342e 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -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
diff --git a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
index 92e2017659..74abb0af2a 100644
--- a/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/SupporterIcon.cs
@@ -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)
diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
index 7337a75509..0ca01ccee6 100644
--- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs
@@ -191,9 +191,14 @@ namespace osu.Game.Rulesets.Edit
}
}
- public IEnumerable CreateTernaryButtons() => new[]
+ public IEnumerable 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)
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 4b64548f9c..9f277b6190 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -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
///
protected abstract IReadOnlyList CompositionTools { get; }
- ///
- /// A collection of states which will be displayed to the user in the toolbox.
- ///
- public TernaryButton[] TernaryStates { get; private set; }
-
///
/// Create all ternary states required to be displayed to the user.
///
- protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
+ protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
///
/// 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;
}
}
diff --git a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs
index 223b770b48..e7161ce36c 100644
--- a/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/ScrollingHitObjectComposer.cs
@@ -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 },
+ }
}
},
});
diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs
index 28e25c72e1..1f91e2c5f0 100644
--- a/osu.Game/Rulesets/UI/ReplayRecorder.cs
+++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs
@@ -27,7 +27,10 @@ namespace osu.Game.Rulesets.UI
private InputManager inputManager;
- public int RecordFrameRate = 60;
+ ///
+ /// The frame rate to record replays at.
+ ///
+ public int RecordFrameRate { get; set; } = 60;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
@@ -76,7 +79,7 @@ namespace osu.Game.Rulesets.UI
{
var last = target.Replay.Frames.LastOrDefault();
- if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate))
+ if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate) * Clock.Rate)
return;
var position = ScreenSpaceToGamefield?.Invoke(inputManager.CurrentState.Mouse.Position) ?? inputManager.CurrentState.Mouse.Position;
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs
index 185e2cab99..5f80c2cd96 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs
@@ -101,18 +101,6 @@ namespace osu.Game.Screens.Backgrounds
}
}
- ///
- /// Reloads beatmap's background.
- ///
- public void RefreshBackground()
- {
- Schedule(() =>
- {
- cancellationSource?.Cancel();
- LoadComponentAsync(new BeatmapBackground(beatmap), switchBackground, (cancellationSource = new CancellationTokenSource()).Token);
- });
- }
-
private void switchBackground(BeatmapBackground b)
{
float newDepth = 0;
diff --git a/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs b/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs
new file mode 100644
index 0000000000..9982357157
--- /dev/null
+++ b/osu.Game/Screens/Backgrounds/EditorBackgroundScreen.cs
@@ -0,0 +1,117 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Backgrounds;
+using osu.Game.Storyboards.Drawables;
+
+namespace osu.Game.Screens.Backgrounds
+{
+ public partial class EditorBackgroundScreen : BackgroundScreen
+ {
+ private readonly WorkingBeatmap beatmap;
+ private readonly Container dimContainer;
+
+ private CancellationTokenSource? cancellationTokenSource;
+ private Bindable dimLevel = null!;
+ private Bindable showStoryboard = null!;
+
+ private BeatmapBackground background = null!;
+ private Container storyboardContainer = null!;
+
+ private IFrameBasedClock? clockSource;
+
+ public EditorBackgroundScreen(WorkingBeatmap beatmap)
+ {
+ this.beatmap = beatmap;
+
+ InternalChild = dimContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ dimContainer.AddRange(createContent());
+ background = dimContainer.OfType().Single();
+ storyboardContainer = dimContainer.OfType().Single();
+
+ dimLevel = config.GetBindable(OsuSetting.EditorDim);
+ showStoryboard = config.GetBindable(OsuSetting.EditorShowStoryboard);
+ }
+
+ private IEnumerable createContent() =>
+ [
+ new BeatmapBackground(beatmap) { RelativeSizeAxes = Axes.Both, },
+ // this kooky container nesting is here because the storyboard needs a custom clock
+ // but also needs it on an isolated-enough level that doesn't break screen stack expiry logic (which happens if the clock was put on `this`),
+ // or doesn't make it literally impossible to fade the storyboard in/out in real time (which happens if the fade transforms were to be applied directly to the storyboard).
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new DrawableStoryboard(beatmap.Storyboard)
+ {
+ Clock = clockSource ?? Clock,
+ }
+ }
+ ];
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ dimLevel.BindValueChanged(_ => dimContainer.FadeColour(OsuColour.Gray(1 - dimLevel.Value), 500, Easing.OutQuint), true);
+ showStoryboard.BindValueChanged(_ => updateState());
+ updateState(0);
+ }
+
+ private void updateState(double duration = 500)
+ {
+ storyboardContainer.FadeTo(showStoryboard.Value ? 1 : 0, duration, Easing.OutQuint);
+ // yes, this causes overdraw, but is also a (crude) fix for bad-looking transitions on screen entry
+ // caused by the previous background on the background stack poking out from under this one and then instantly fading out
+ background.FadeColour(beatmap.Storyboard.ReplacesBackground && showStoryboard.Value ? Colour4.Black : Colour4.White, duration, Easing.OutQuint);
+ }
+
+ public void ChangeClockSource(IFrameBasedClock frameBasedClock)
+ {
+ clockSource = frameBasedClock;
+ if (IsLoaded)
+ storyboardContainer.Child.Clock = frameBasedClock;
+ }
+
+ public void RefreshBackground()
+ {
+ cancellationTokenSource?.Cancel();
+ LoadComponentsAsync(createContent(), loaded =>
+ {
+ dimContainer.Clear();
+ dimContainer.AddRange(loaded);
+
+ background = dimContainer.OfType().Single();
+ storyboardContainer = dimContainer.OfType().Single();
+ updateState(0);
+ }, (cancellationTokenSource ??= new CancellationTokenSource()).Token);
+ }
+
+ public override bool Equals(BackgroundScreen? other)
+ {
+ if (other is not EditorBackgroundScreen otherBeatmapBackground)
+ return false;
+
+ return base.Equals(other) && beatmap == otherBeatmapBackground.beatmap;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
index da71457004..37337bc79f 100644
--- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
+++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -18,8 +17,6 @@ namespace osu.Game.Screens.Edit.Components
protected readonly IBindable Beatmap = new Bindable();
- protected readonly IBindable