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 a0d96c7bb4..4a5fc6218e 100644
--- a/osu.Desktop/Windows/WindowsAssociationManager.cs
+++ b/osu.Desktop/Windows/WindowsAssociationManager.cs
@@ -61,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)
@@ -81,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)
@@ -100,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)
@@ -152,19 +155,6 @@ namespace osu.Desktop.Windows
application_capability.RegisterUriAssociations(uri_associations);
}
- private static void updateDescriptions(LocalisationManager? localisation)
- {
- application_capability.UpdateDescription(getLocalisedString(application_capability.Description));
-
- foreach (var association in file_associations)
- association.UpdateDescription(getLocalisedString(association.Description));
-
- foreach (var association in uri_associations)
- association.UpdateDescription(getLocalisedString(association.Description));
-
- string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString();
- }
-
#region Native interop
[DllImport("Shell32.dll")]
@@ -188,26 +178,37 @@ namespace osu.Desktop.Windows
#endregion
- private record ApplicationCapability(string UniqueName, string CapabilityPath, LocalisableString Description)
+ private class ApplicationCapability
{
+ 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 (Registry.CurrentUser.CreateSubKey(CapabilityPath))
+ using (var capability = Registry.CurrentUser.CreateSubKey(capabilityPath))
{
- // create an empty "capability" key, other methods will fill it with information
+ capability.SetValue(@"ApplicationDescription", description.ToString());
}
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
- registeredApplications?.SetValue(UniqueName, CapabilityPath);
+ registeredApplications?.SetValue(uniqueName, capabilityPath);
}
public void RegisterFileAssociations(FileAssociation[] associations)
{
- using var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true);
+ using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
if (capability == null) return;
using var fileAssociations = capability.CreateSubKey(@"FileAssociations");
@@ -218,7 +219,7 @@ namespace osu.Desktop.Windows
public void RegisterUriAssociations(UriAssociation[] associations)
{
- using var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true);
+ using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
if (capability == null) return;
using var urlAssociations = capability.CreateSubKey(@"UrlAssociations");
@@ -227,27 +228,38 @@ namespace osu.Desktop.Windows
urlAssociations.SetValue(association.Protocol, association.ProgramId);
}
- public void UpdateDescription(string description)
+ public void LocaliseDescription(LocalisationManager localisationManager)
{
- using (var capability = Registry.CurrentUser.OpenSubKey(CapabilityPath, true))
+ using (var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true))
{
- capability?.SetValue(@"ApplicationDescription", description);
+ capability?.SetValue(@"ApplicationDescription", localisationManager.GetLocalisedString(description));
}
}
public void Uninstall()
{
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
- registeredApplications?.DeleteValue(UniqueName, throwOnMissingValue: false);
+ registeredApplications?.DeleteValue(uniqueName, throwOnMissingValue: false);
- Registry.CurrentUser.DeleteSubKeyTree(CapabilityPath, throwOnMissingSubKey: false);
+ Registry.CurrentUser.DeleteSubKeyTree(capabilityPath, throwOnMissingSubKey: false);
}
}
- private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
+ 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
///
@@ -259,8 +271,10 @@ namespace osu.Desktop.Windows
// register a program id for the given extension
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""");
@@ -280,13 +294,13 @@ namespace osu.Desktop.Windows
}
}
- 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);
+ programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
}
///
@@ -307,13 +321,24 @@ namespace osu.Desktop.Windows
}
}
- 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}";
@@ -327,7 +352,8 @@ 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);
// clear out old data
protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false);
@@ -338,20 +364,20 @@ namespace osu.Desktop.Windows
using (var programKey = classes.CreateSubKey(ProgramId))
{
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""");
}
}
- 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()
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/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.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/Mods/OsuModTargetPractice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
index a5846efdfe..72422a0ae8 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
@@ -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();
}
}
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/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
index c7cf3fe956..ee2733ad91 100644
--- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
+++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs
@@ -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));
+ }
}
}
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..04dae38668 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
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;
@@ -127,6 +128,35 @@ namespace osu.Game.Tests.Visual.Editing
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/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
index b09dbc1a91..2b0717c1e3 100644
--- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
+++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs
@@ -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();
}
}
diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs
new file mode 100644
index 0000000000..a5590c79ae
--- /dev/null
+++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroIntegrity.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . 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>().Value = new DummyWorkingBeatmap(Audio, null));
+ AddStep("invalidate beatmap from cache", () => Dependencies.Get().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();
+ }
+}
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/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs
index 19273e3714..5a7c28d024 100644
--- a/osu.Game/Audio/HitSampleInfo.cs
+++ b/osu.Game/Audio/HitSampleInfo.cs
@@ -33,12 +33,12 @@ namespace osu.Game.Audio
///
/// All valid sample addition constants.
///
- public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_FINISH, HIT_CLAP };
+ public static readonly string[] ALL_ADDITIONS = [HIT_WHISTLE, HIT_FINISH, HIT_CLAP];
///
/// All valid bank constants.
///
- public static IEnumerable AllBanks => new[] { BANK_NORMAL, BANK_SOFT, BANK_DRUM };
+ public static readonly string[] ALL_BANKS = [BANK_NORMAL, BANK_SOFT, BANK_DRUM];
///
/// The name of the sample to load.
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/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 8027b6bfbc..5e247ca877 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -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(BeatmapManager);
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
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/Checks/CheckDelayedHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs
index d6cd4f4caa..ee950248db 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckDelayedHitsounds.cs
@@ -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
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
index 3358e81d5f..97c1519c24 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
@@ -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
diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
index 9b8375f208..f5146d1675 100644
--- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
+++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
@@ -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,
+ };
}
}
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
private void createStateBindables()
{
- foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO))
+ foreach (string bankName in HitSampleInfo.ALL_BANKS.Prepend(HIT_BANK_AUTO))
{
var bindable = new Bindable
{
@@ -143,7 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBankStates[bankName] = bindable;
}
- foreach (string bankName in HitSampleInfo.AllBanks.Prepend(HIT_BANK_AUTO))
+ foreach (string bankName in HitSampleInfo.ALL_BANKS.Prepend(HIT_BANK_AUTO))
{
var bindable = new Bindable
{
@@ -216,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
resetTernaryStates();
- foreach (string sampleName in HitSampleInfo.AllAdditions)
+ foreach (string sampleName in HitSampleInfo.ALL_ADDITIONS)
{
var bindable = new Bindable
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
index c3a56c8df9..4ca3f93f13 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs
@@ -409,7 +409,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void createStateBindables()
{
- foreach (string sampleName in HitSampleInfo.AllAdditions)
+ foreach (string sampleName in HitSampleInfo.ALL_ADDITIONS)
{
var bindable = new Bindable
{
@@ -433,7 +433,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
selectionSampleStates[sampleName] = bindable;
}
- banks.AddRange(HitSampleInfo.AllBanks.Prepend(EditorSelectionHandler.HIT_BANK_AUTO));
+ banks.AddRange(HitSampleInfo.ALL_BANKS.Prepend(EditorSelectionHandler.HIT_BANK_AUTO));
}
private void updateTernaryStates()
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 66621afa21..e5360e2eeb 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -3,9 +3,9 @@
using System;
using osu.Framework.Allocation;
-using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
@@ -49,6 +49,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
+ [Resolved]
+ private IBindable beatmap { get; set; } = null!;
+
///
/// The timeline's scroll position in the last frame.
///
@@ -86,8 +89,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private double trackLengthForZoom;
- private readonly IBindable track = new Bindable();
-
public Timeline(Drawable userContent)
{
this.userContent = userContent;
@@ -101,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
[BackgroundDependencyLoader]
- private void load(IBindable beatmap, OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config)
+ private void load(OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config)
{
CentreMarker centreMarker;
@@ -150,16 +151,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
controlPointsVisible = config.GetBindable(OsuSetting.EditorTimelineShowTimingChanges);
ticksVisible = config.GetBindable(OsuSetting.EditorTimelineShowTicks);
- track.BindTo(editorClock.Track);
- track.BindValueChanged(_ =>
- {
- waveform.Waveform = beatmap.Value.Waveform;
- Scheduler.AddOnce(applyVisualOffset, beatmap);
- }, true);
+ editorClock.TrackChanged += updateWaveform;
+ updateWaveform();
Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom);
}
+ private void updateWaveform()
+ {
+ waveform.Waveform = beatmap.Value.Waveform;
+ Scheduler.AddOnce(applyVisualOffset, beatmap);
+ }
+
private void applyVisualOffset(IBindable beatmap)
{
waveform.RelativePositionAxes = Axes.X;
@@ -334,5 +337,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);
return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time));
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (editorClock.IsNotNull())
+ editorClock.TrackChanged -= updateWaveform;
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
index 578e945c64..2b5667ff9c 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs
@@ -155,9 +155,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
break;
- foreach (var sample in hitObject.Samples)
+ for (int i = 0; i < hitObject.Samples.Count; i++)
{
- if (!HitSampleInfo.AllBanks.Contains(sample.Bank))
+ var sample = hitObject.Samples[i];
+
+ if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank))
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
}
@@ -165,10 +167,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2);
- foreach (var sample in hasRepeats.NodeSamples.SelectMany(s => s))
+ for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
{
- if (!HitSampleInfo.AllBanks.Contains(sample.Bank))
- minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
+ var node = hasRepeats.NodeSamples[i];
+
+ for (int j = 0; j < node.Count; j++)
+ {
+ var sample = node[j];
+
+ if (!HitSampleInfo.ALL_BANKS.Contains(sample.Bank))
+ minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index f6875a7aa4..a77696bc45 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -861,6 +861,7 @@ namespace osu.Game.Screens.Edit
{
base.OnResuming(e);
dimBackground();
+ clock.BindAdjustments();
}
private void dimBackground()
@@ -925,6 +926,10 @@ namespace osu.Game.Screens.Edit
base.OnSuspending(e);
clock.Stop();
refetchBeatmap();
+ // unfortunately ordering matters here.
+ // this unbind MUST happen after `refetchBeatmap()`, because along other things, `refetchBeatmap()` causes a global working beatmap change,
+ // which causes `EditorClock` to reload the track and automatically reapply adjustments to it.
+ clock.UnbindAdjustments();
}
private void refetchBeatmap()
diff --git a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs
index 4fe431498f..957c1d0969 100644
--- a/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs
+++ b/osu.Game/Screens/Edit/EditorBeatmapProcessor.cs
@@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit
rulesetBeatmapProcessor?.PostProcess();
autoGenerateBreaks();
+ ensureNewComboAfterBreaks();
}
private void autoGenerateBreaks()
@@ -100,5 +101,40 @@ namespace osu.Game.Screens.Edit
Beatmap.Breaks.Add(breakPeriod);
}
}
+
+ private void ensureNewComboAfterBreaks()
+ {
+ var breakEnds = Beatmap.Breaks.Select(b => b.EndTime).OrderBy(t => t).ToList();
+
+ if (breakEnds.Count == 0)
+ return;
+
+ int currentBreak = 0;
+
+ IHasComboInformation? lastObj = null;
+ bool comboInformationUpdateRequired = false;
+
+ foreach (var hitObject in Beatmap.HitObjects)
+ {
+ if (hitObject is not IHasComboInformation hasCombo)
+ continue;
+
+ if (currentBreak < breakEnds.Count && hitObject.StartTime >= breakEnds[currentBreak])
+ {
+ if (!hasCombo.NewCombo)
+ {
+ hasCombo.NewCombo = true;
+ comboInformationUpdateRequired = true;
+ }
+
+ currentBreak += 1;
+ }
+
+ if (comboInformationUpdateRequired)
+ hasCombo.UpdateComboInformation(lastObj);
+
+ lastObj = hasCombo;
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 5b9c662c95..8b9bdb595d 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -6,6 +6,8 @@
using System;
using System.Diagnostics;
using System.Linq;
+using JetBrains.Annotations;
+using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -23,12 +25,15 @@ namespace osu.Game.Screens.Edit
///
public partial class EditorClock : CompositeComponent, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{
- public IBindable Track => track;
+ [CanBeNull]
+ public event Action TrackChanged;
private readonly Bindable track = new Bindable();
public double TrackLength => track.Value?.IsLoaded == true ? track.Value.Length : 60000;
+ public AudioAdjustments AudioAdjustments { get; } = new AudioAdjustments();
+
public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo;
public IBeatmap Beatmap { get; set; }
@@ -56,6 +61,8 @@ namespace osu.Game.Screens.Edit
underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true);
AddInternal(underlyingClock);
+
+ track.BindValueChanged(_ => TrackChanged?.Invoke());
}
///
@@ -208,7 +215,16 @@ namespace osu.Game.Screens.Edit
}
}
- public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments();
+ public void BindAdjustments() => track.Value?.BindAdjustments(AudioAdjustments);
+
+ public void UnbindAdjustments() => track.Value?.UnbindAdjustments(AudioAdjustments);
+
+ public void ResetSpeedAdjustments()
+ {
+ AudioAdjustments.RemoveAllAdjustments(AdjustableProperty.Frequency);
+ AudioAdjustments.RemoveAllAdjustments(AdjustableProperty.Tempo);
+ underlyingClock.ResetSpeedAdjustments();
+ }
double IAdjustableClock.Rate
{
@@ -231,8 +247,12 @@ namespace osu.Game.Screens.Edit
public void ChangeSource(IClock source)
{
+ UnbindAdjustments();
+
track.Value = source as Track;
underlyingClock.ChangeSource(source);
+
+ BindAdjustments();
}
public IClock Source => underlyingClock.Source;
diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
index 45213b7bdb..2df2dd7c5b 100644
--- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
+++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs
@@ -4,8 +4,8 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
@@ -305,7 +305,8 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private IBindable beatmap { get; set; } = null!;
- private readonly IBindable track = new Bindable();
+ [Resolved]
+ private EditorClock editorClock { get; set; } = null!;
public WaveformRow(bool isMainRow)
{
@@ -313,7 +314,7 @@ namespace osu.Game.Screens.Edit.Timing
}
[BackgroundDependencyLoader]
- private void load(EditorClock clock)
+ private void load()
{
InternalChildren = new Drawable[]
{
@@ -343,13 +344,16 @@ namespace osu.Game.Screens.Edit.Timing
Colour = colourProvider.Content2
}
};
-
- track.BindTo(clock.Track);
}
protected override void LoadComplete()
{
- track.ValueChanged += _ => waveformGraph.Waveform = beatmap.Value.Waveform;
+ editorClock.TrackChanged += updateWaveform;
+ }
+
+ private void updateWaveform()
+ {
+ waveformGraph.Waveform = beatmap.Value.Waveform;
}
public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
@@ -363,6 +367,14 @@ namespace osu.Game.Screens.Edit.Timing
get => waveformGraph.X;
set => waveformGraph.X = value;
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (editorClock.IsNotNull())
+ editorClock.TrackChanged -= updateWaveform;
+ }
}
}
}
diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs
index c110c53df8..7b23cc7538 100644
--- a/osu.Game/Screens/Menu/IntroScreen.cs
+++ b/osu.Game/Screens/Menu/IntroScreen.cs
@@ -20,6 +20,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
+using osu.Game.Extensions;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays;
@@ -170,7 +171,14 @@ namespace osu.Game.Screens.Menu
if (s.Beatmaps.Count == 0)
return;
- initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps.First());
+ var working = beatmaps.GetWorkingBeatmap(s.Beatmaps.First());
+
+ // Ensure files area actually present on disk.
+ // This is to handle edge cases like users deleting files outside the game and breaking the world.
+ if (!hasAllFiles(working))
+ return;
+
+ initialBeatmap = working;
});
return UsingThemedIntro = initialBeatmap != null;
@@ -188,6 +196,20 @@ namespace osu.Game.Screens.Menu
[Resolved]
private INotificationOverlay notifications { get; set; }
+ private bool hasAllFiles(WorkingBeatmap working)
+ {
+ foreach (var f in working.BeatmapSetInfo.Files)
+ {
+ using (var str = working.GetStream(f.File.GetStoragePath()))
+ {
+ if (str == null)
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private void ensureEventuallyArrivingAtMenu()
{
// This intends to handle the case where an intro may get stuck.
diff --git a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs
index 2bdb41ce12..e2aecb6781 100644
--- a/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/StarRatingRangeDisplay.cs
@@ -14,7 +14,6 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osuTK;
-using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Screens.OnlinePlay.Components
{
@@ -30,6 +29,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
private StarRatingDisplay maxDisplay = null!;
private Drawable maxBackground = null!;
+ private BufferedContainer bufferedContent = null!;
+
public StarRatingRangeDisplay(Room room)
{
this.room = room;
@@ -41,38 +42,43 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
InternalChildren = new Drawable[]
{
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- CornerRadius = 1,
- Children = new[]
- {
- minBackground = new Box
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- RelativeSizeAxes = Axes.Both,
- Size = new Vector2(0.5f),
- },
- maxBackground = new Box
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- RelativeSizeAxes = Axes.Both,
- Size = new Vector2(0.5f),
- },
- }
- },
- new FillFlowContainer
+ new CircularContainer
{
AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
+ Masking = true,
+ // Stops artifacting from boxes drawn behind wrong colour boxes (and edge pixels adding up to higher opacity).
+ Padding = new MarginPadding(-0.1f),
+ Child = bufferedContent = new BufferedContainer(pixelSnapping: true, cachedFrameBuffer: true)
{
- minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range),
- maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range)
+ AutoSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ minBackground = new Box
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(1, 0.5f),
+ },
+ maxBackground = new Box
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(1, 0.5f),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range),
+ maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range)
+ }
+ }
+ }
}
- }
+ },
};
}
@@ -121,6 +127,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
minBackground.Colour = colours.ForStarDifficulty(minDifficulty.Stars);
maxBackground.Colour = colours.ForStarDifficulty(maxDifficulty.Stars);
+
+ bufferedContent.ForceRedraw();
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs
index a8e0fc9030..a792424562 100644
--- a/osu.Game/Users/UserActivity.cs
+++ b/osu.Game/Users/UserActivity.cs
@@ -54,6 +54,10 @@ namespace osu.Game.Users
}
[MessagePackObject]
+ [Union(12, typeof(InSoloGame))]
+ [Union(23, typeof(InMultiplayerGame))]
+ [Union(24, typeof(SpectatingMultiplayerGame))]
+ [Union(31, typeof(InPlaylistGame))]
public abstract class InGame : UserActivity
{
[Key(0)]
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 252d99e6dd..bcca1eee35 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -20,24 +20,24 @@
-
+
-
-
-
-
-
-
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+