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-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs
index 9cd18d2d9f..0699f5d039 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/Objects/EmptyFreeformHitObject.cs
@@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
public Vector2 Position { get; set; }
- public float X => Position.X;
- public float Y => Position.Y;
+ public float X
+ {
+ get => Position.X;
+ set => Position = new Vector2(value, Y);
+ }
+
+ public float Y
+ {
+ get => Position.Y;
+ set => Position = new Vector2(X, value);
+ }
}
}
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-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs
index 0c22554e82..f938d26b26 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/Objects/PippidonHitObject.cs
@@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
public Vector2 Position { get; set; }
- public float X => Position.X;
- public float Y => Position.Y;
+ public float X
+ {
+ get => Position.X;
+ set => Position = new Vector2(value, Y);
+ }
+
+ public float Y
+ {
+ get => Position.Y;
+ set => Position = new Vector2(X, value);
+ }
}
}
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/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.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index 329055b3dd..2018fd5ea9 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -210,11 +210,27 @@ namespace osu.Game.Rulesets.Catch.Objects
///
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
- float IHasXPosition.X => OriginalX;
+ float IHasXPosition.X
+ {
+ get => OriginalX;
+ set => OriginalX = value;
+ }
- float IHasYPosition.Y => LegacyConvertedY;
+ float IHasYPosition.Y
+ {
+ get => LegacyConvertedY;
+ set => LegacyConvertedY = value;
+ }
- Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
+ Vector2 IHasPosition.Position
+ {
+ get => new Vector2(OriginalX, LegacyConvertedY);
+ set
+ {
+ ((IHasXPosition)this).X = value.X;
+ ((IHasYPosition)this).Y = value.Y;
+ }
+ }
#endregion
}
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.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
index 25ad6b997d..c8c8867bc6 100644
--- a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs
@@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
#region LegacyBeatmapEncoder
- float IHasXPosition.X => Column;
+ float IHasXPosition.X
+ {
+ get => Column;
+ set => Column = (int)value;
+ }
#endregion
}
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 eacd2b3e75..e22e1d2001 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -377,13 +377,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
UpdateState(ArmedState.Idle);
HeadCircle.SuppressHitAnimations();
+
+ foreach (var repeat in repeatContainer)
+ repeat.SuppressHitAnimations();
+
TailCircle.SuppressHitAnimations();
+
+ // This method is called every frame in editor contexts, thus the lack of need for transforms.
+
+ if (Time.Current >= HitStateUpdateTime)
+ {
+ // Apply the slider's alpha to *only* the body.
+ // This allows start and – more importantly – end circles to fade slower than the overall slider.
+ if (Alpha < 1)
+ Body.Alpha = Alpha;
+ Alpha = 1;
+ }
+
+ LifetimeEnd = HitStateUpdateTime + 700;
}
internal void RestoreHitAnimations()
{
UpdateState(ArmedState.Hit);
HeadCircle.RestoreHitAnimations();
+
+ foreach (var repeat in repeatContainer)
+ repeat.RestoreHitAnimations();
+
TailCircle.RestoreHitAnimations();
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 27c5278614..bc48f34828 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -163,5 +164,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
+
+ #region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
+
+ internal void SuppressHitAnimations()
+ {
+ UpdateState(ArmedState.Idle);
+ UpdateComboColour();
+
+ // This method is called every frame in editor contexts, thus the lack of need for transforms.
+
+ bool hit = Time.Current >= HitStateUpdateTime;
+
+ if (hit)
+ {
+ // More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
+ AccentColour.Value = Color4.White;
+ Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
+ }
+
+ Arrow.Alpha = hit ? 0 : 1;
+
+ LifetimeEnd = HitStateUpdateTime + 700;
+ }
+
+ internal void RestoreHitAnimations()
+ {
+ UpdateState(ArmedState.Hit);
+ UpdateComboColour();
+ Arrow.Alpha = 1;
+ }
+
+ #endregion
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index 1b0993b698..8c1bd6302e 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
set => position.Value = value;
}
- public float X => Position.X;
- public float Y => Position.Y;
+ public float X
+ {
+ get => Position.X;
+ set => Position = new Vector2(value, Position.Y);
+ }
+
+ public float Y
+ {
+ get => Position.Y;
+ set => Position = new Vector2(Position.X, value);
+ }
public Vector2 StackedPosition => Position + StackOffset;
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/IO/LegacyBeatmapExporterTest.cs b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
index 8a95d26782..cf498c7856 100644
--- a/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/LegacyBeatmapExporterTest.cs
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using MemoryStream = System.IO.MemoryStream;
@@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
}
+ [Test]
+ public void TestFractionalObjectCoordinatesRounded()
+ {
+ IWorkingBeatmap beatmap = null!;
+ MemoryStream outStream = null!;
+
+ // Ensure importer encoding is correct
+ AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz"));
+ AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001));
+
+ // Ensure exporter legacy conversion is correct
+ AddStep("export", () =>
+ {
+ outStream = new MemoryStream();
+
+ new LegacyBeatmapExporter(LocalStorage)
+ .ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
+ });
+
+ AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
+ AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001));
+ }
+
[Test]
public void TestExportStability()
{
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/Resources/Archives/fractional-coordinates.olz b/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz
new file mode 100644
index 0000000000..5c5af368c8
Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/fractional-coordinates.olz differ
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
index 0f47c3cd27..aa99b22701 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
@@ -27,18 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUpSteps]
public void SetUpSteps()
{
- AddStep("Create control", () =>
- {
- Child = new PlayerSettingsGroup("Some settings")
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new Drawable[]
- {
- offsetControl = new BeatmapOffsetControl()
- }
- };
- });
+ recreateControl();
}
[Test]
@@ -123,13 +112,14 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestCalibrationFromZero()
{
+ ScoreInfo referenceScore = null!;
const double average_error = -4.5;
AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
AddStep("Set reference score", () =>
{
- offsetControl.ReferenceScore.Value = new ScoreInfo
+ offsetControl.ReferenceScore.Value = referenceScore = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error),
BeatmapInfo = Beatmap.Value.BeatmapInfo,
@@ -143,6 +133,10 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+
+ recreateControl();
+ AddStep("Set same reference score", () => offsetControl.ReferenceScore.Value = referenceScore);
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
}
///
@@ -251,5 +245,21 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
}
+
+ private void recreateControl()
+ {
+ AddStep("Create control", () =>
+ {
+ Child = new PlayerSettingsGroup("Some settings")
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ offsetControl = new BeatmapOffsetControl()
+ }
+ };
+ });
+ }
}
}
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/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 5646649d33..58e780cf16 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -355,18 +355,18 @@ namespace osu.Game.Tests.Visual.Navigation
}
[Test]
- public void TestLastScoreNullAfterExitingPlayer()
+ public void TestLastScoreNotNullAfterExitingPlayer()
{
- AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
+ AddUntilStep("last play null", getLastPlay, () => Is.Null);
var getOriginalPlayer = playToCompletion();
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action());
- AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
+ AddUntilStep("last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit());
- AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
+ AddUntilStep("last play not null", getLastPlay, () => Is.Not.Null);
ScoreInfo getLastPlay() => Game.Dependencies.Get().Get(Static.LastLocalUserScore);
}
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/Beatmaps/MetadataUtils.cs b/osu.Game/Beatmaps/MetadataUtils.cs
index 89c821c16c..1d2a3b5d01 100644
--- a/osu.Game/Beatmaps/MetadataUtils.cs
+++ b/osu.Game/Beatmaps/MetadataUtils.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps
/// Returns if the character can be used in and fields.
/// Characters not matched by this method can be placed in and .
///
- public static bool IsRomanised(char c) => c <= 0xFF;
+ public static bool IsRomanised(char c) => char.IsAscii(c) && !char.IsControl(c);
///
/// Returns if the string can be used in and fields.
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index f050a2338a..6f32e1e7fb 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
+using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
@@ -163,6 +164,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Version, string.Empty);
SetDefault(OsuSetting.ShowFirstRunSetup, true);
+ SetDefault(OsuSetting.ShowMobileDisclaimer, RuntimeInfo.IsMobile);
SetDefault(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg);
SetDefault(OsuSetting.ScreenshotCaptureMenuCursor, false);
@@ -453,6 +455,7 @@ namespace osu.Game.Configuration
AlwaysRequireHoldingForPause,
MultiplayerShowInProgressFilter,
BeatmapListingFeaturedArtistFilter,
+ ShowMobileDisclaimer,
EditorShowStoryboard,
}
}
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index 18631f5d00..c55a597c32 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.cs
@@ -10,7 +10,6 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Scoring;
-using osu.Game.Screens.Play;
namespace osu.Game.Configuration
{
@@ -30,6 +29,7 @@ namespace osu.Game.Configuration
SetDefault(Static.SeasonalBackgrounds, null);
SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile);
SetDefault(Static.LastLocalUserScore, null);
+ SetDefault(Static.LastAppliedOffsetScore, null);
}
///
@@ -78,11 +78,15 @@ namespace osu.Game.Configuration
TouchInputActive,
///
- /// Contains the local user's last score (can be completed or aborted) after exiting .
- /// Will be cleared to null when leaving .
+ /// Stores the local user's last score (can be completed or aborted).
///
LastLocalUserScore,
+ ///
+ /// Stores the local user's last score which was used to apply an offset.
+ ///
+ LastAppliedOffsetScore,
+
///
/// Whether the intro animation for the daily challenge screen has been played once.
/// This is reset when a new challenge is up.
diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs
index eb48425588..24e752da31 100644
--- a/osu.Game/Database/LegacyBeatmapExporter.cs
+++ b/osu.Game/Database/LegacyBeatmapExporter.cs
@@ -42,7 +42,10 @@ namespace osu.Game.Database
return null;
using var contentStreamReader = new LineBufferedReader(contentStream);
- var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader);
+
+ // FIRST_LAZER_VERSION is specified here to avoid flooring object coordinates on decode via `(int)` casts.
+ // we will be making integers out of them lower down, but in a slightly different manner (rounding rather than truncating)
+ var beatmapContent = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION).Decode(contentStreamReader);
var workingBeatmap = new FlatWorkingBeatmap(beatmapContent);
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
@@ -93,6 +96,12 @@ namespace osu.Game.Database
hitObject.StartTime = Math.Floor(hitObject.StartTime);
+ if (hitObject is IHasXPosition hasXPosition)
+ hasXPosition.X = MathF.Round(hasXPosition.X);
+
+ if (hitObject is IHasYPosition hasYPosition)
+ hasYPosition.Y = MathF.Round(hasYPosition.Y);
+
if (hitObject is not IHasPath hasPath) continue;
// stable's hit object parsing expects the entire slider to use only one type of curve,
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/ButtonSystemStrings.cs b/osu.Game/Localisation/ButtonSystemStrings.cs
index b0a205eebe..a9bc3068da 100644
--- a/osu.Game/Localisation/ButtonSystemStrings.cs
+++ b/osu.Game/Localisation/ButtonSystemStrings.cs
@@ -59,6 +59,25 @@ namespace osu.Game.Localisation
///
public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge");
+ ///
+ /// "A few important words from your dev team!"
+ ///
+ public static LocalisableString MobileDisclaimerHeader => new TranslatableString(getKey(@"mobile_disclaimer_header"), @"A few important words from your dev team!");
+
+ ///
+ /// "While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version.
+ ///
+ /// Your experience will not be perfect, and may even feel subpar compared to games which are made mobile-first.
+ ///
+ /// Please bear with us as we continue to improve the game for you!"
+ ///
+ public static LocalisableString MobileDisclaimerBody => new TranslatableString(getKey(@"mobile_disclaimer_body"),
+ @"While we have released osu! on mobile platforms to maximise the number of people that can enjoy the game, our focus is still on the PC version.
+
+Your experience will not be perfect, and may even feel subpar compared to games which are made mobile-first.
+
+Please bear with us as we continue to improve the game for you!");
+
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
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/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs
index a23c394c9f..0fec1625eb 100644
--- a/osu.Game/Overlays/Dialog/PopupDialog.cs
+++ b/osu.Game/Overlays/Dialog/PopupDialog.cs
@@ -75,7 +75,9 @@ namespace osu.Game.Overlays.Dialog
return;
bodyText = value;
+
body.Text = value;
+ body.TextAnchor = bodyText.ToString().Contains('\n') ? Anchor.TopLeft : Anchor.TopCentre;
}
}
@@ -210,13 +212,12 @@ namespace osu.Game.Overlays.Dialog
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.TopCentre,
- Padding = new MarginPadding { Horizontal = 15 },
+ Padding = new MarginPadding { Horizontal = 15, Bottom = 10 },
},
body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18))
{
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
- TextAnchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 15 },
@@ -301,6 +302,7 @@ namespace osu.Game.Overlays.Dialog
{
content.ScaleTo(0.7f);
ring.ResizeTo(ringMinifiedSize);
+ icon.ScaleTo(0f);
}
content
@@ -308,6 +310,7 @@ namespace osu.Game.Overlays.Dialog
.FadeIn(ENTER_DURATION, Easing.OutQuint);
ring.ResizeTo(ringSize, ENTER_DURATION * 1.5f, Easing.OutQuint);
+ icon.Delay(100).ScaleTo(1, ENTER_DURATION * 1.5f, Easing.OutQuint);
}
protected override void PopOut()
diff --git a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs
index 77835b1f09..3022233e9c 100644
--- a/osu.Game/Overlays/Login/SecondFactorAuthForm.cs
+++ b/osu.Game/Overlays/Login/SecondFactorAuthForm.cs
@@ -121,9 +121,11 @@ namespace osu.Game.Overlays.Login
codeTextBox.Current.BindValueChanged(code =>
{
- if (code.NewValue.Length == 8)
+ string trimmedCode = code.NewValue.Trim();
+
+ if (trimmedCode.Length == 8)
{
- api.AuthenticateSecondFactor(code.NewValue);
+ api.AuthenticateSecondFactor(trimmedCode);
codeTextBox.Current.Disabled = true;
}
});
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/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
index 8af795f880..2a94ae6017 100644
--- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
+++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
@@ -55,12 +55,6 @@ namespace osu.Game.Rulesets.Edit
}
}
- protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos);
-
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos);
-
- private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.ScreenSpaceDrawQuad.Contains(screenSpacePos);
-
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs
index ced9b24ebf..091b0a1e6f 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs
@@ -21,9 +21,17 @@ namespace osu.Game.Rulesets.Objects.Legacy
public int ComboOffset { get; set; }
- public float X => Position.X;
+ public float X
+ {
+ get => Position.X;
+ set => Position = new Vector2(value, Position.Y);
+ }
- public float Y => Position.Y;
+ public float Y
+ {
+ get => Position.Y;
+ set => Position = new Vector2(Position.X, value);
+ }
public Vector2 Position { get; set; }
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/Rulesets/Objects/Types/IHasPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs
index 8948fe59a9..e9b3cc46eb 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasPosition.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasPosition.cs
@@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Types
///
/// The starting position of the HitObject.
///
- Vector2 Position { get; }
+ Vector2 Position { get; set; }
}
}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs
index 7e55b21050..18f1f996e3 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasXPosition.cs
@@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
///
/// The starting X-position of this HitObject.
///
- float X { get; }
+ float X { get; set; }
}
}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs b/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs
index d2561b10a7..dcaeaf594a 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasYPosition.cs
@@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
///
/// The starting Y-position of this HitObject.
///
- float Y { get; }
+ float Y { get; set; }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs
index 78cee2c1cf..cd6e25734a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
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/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/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs
index 29e730c865..5e5b740b62 100644
--- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs
+++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Framework.Utils;
@@ -41,6 +42,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
+ [Resolved]
+ private BindableBeatDivisor beatDivisor { get; set; } = null!;
+
public bool EnableClicking
{
get => metronomeTick.EnableClicking;
@@ -222,7 +226,7 @@ namespace osu.Game.Screens.Edit.Timing
Clock = new FramedClock(metronomeClock = new StopwatchClock(true));
}
- private double beatLength;
+ private double effectiveBeatLength;
private TimingControlPoint timingPoint = null!;
@@ -232,11 +236,26 @@ namespace osu.Game.Screens.Edit.Timing
private ScheduledDelegate? latchDelegate;
+ private bool spedUp;
+
+ private int computeSpedUpDivisor()
+ {
+ if (!spedUp)
+ return 1;
+
+ if (beatDivisor.Value % 3 == 0)
+ return 3;
+ if (beatDivisor.Value % 2 == 0)
+ return 2;
+
+ return 1;
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
- interpolatedBpm.BindValueChanged(bpm => bpmText.Text = bpm.NewValue.ToLocalisableString());
+ interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString());
}
protected override void Update()
@@ -250,16 +269,20 @@ namespace osu.Game.Screens.Edit.Timing
timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime);
- if (beatLength != timingPoint.BeatLength)
+ Divisor = metronomeTick.Divisor = computeSpedUpDivisor();
+
+ if (effectiveBeatLength != timingPoint.BeatLength / Divisor)
{
- beatLength = timingPoint.BeatLength;
+ effectiveBeatLength = timingPoint.BeatLength / Divisor;
EarlyActivationMilliseconds = timingPoint.BeatLength / 2;
- float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((timingPoint.BPM - 30) / 480, 0, 1));
+ double effectiveBpm = 60000 / effectiveBeatLength;
+
+ float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((effectiveBpm - 30) / 480, 0, 1));
weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint);
- this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint);
+ this.TransformBindableTo(interpolatedBpm, (int)Math.Round(effectiveBpm), 600, Easing.OutQuint);
}
if (!BeatSyncSource.Clock.IsRunning && isSwinging)
@@ -305,7 +328,7 @@ namespace osu.Game.Screens.Edit.Timing
float currentAngle = swing.Rotation;
float targetAngle = currentAngle > 0 ? -angle : angle;
- swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad);
+ swing.RotateTo(targetAngle, effectiveBeatLength, Easing.InOutQuad);
}
private void onTickPlayed()
@@ -313,9 +336,25 @@ namespace osu.Game.Screens.Edit.Timing
// Originally, this flash only occurred when the pendulum correctly passess the centre.
// Mappers weren't happy with the metronome tick not playing immediately after starting playback
// so now this matches the actual tick sample.
- stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
+ stick.FlashColour(overlayColourProvider.Content1, effectiveBeatLength, Easing.OutQuint);
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ updateDivisorFromKey(e);
+
+ return base.OnKeyDown(e);
+ }
+
+ protected override void OnKeyUp(KeyUpEvent e)
+ {
+ base.OnKeyUp(e);
+
+ updateDivisorFromKey(e);
+ }
+
+ private void updateDivisorFromKey(UIEvent e) => spedUp = e.ControlPressed;
+
private partial class MetronomeTick : BeatSyncedContainer
{
public bool EnableClicking;
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/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 99bc1825f5..135b3dba17 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -13,11 +13,13 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
+using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
@@ -39,6 +41,7 @@ using osu.Game.Screens.Select;
using osu.Game.Seasonal;
using osuTK;
using osuTK.Graphics;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Menu
{
@@ -87,6 +90,7 @@ namespace osu.Game.Screens.Menu
private Bindable holdDelay;
private Bindable loginDisplayed;
+ private Bindable showMobileDisclaimer;
private HoldToExitGameOverlay holdToExitGameOverlay;
@@ -111,6 +115,7 @@ namespace osu.Game.Screens.Menu
{
holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay);
loginDisplayed = statics.GetBindable(Static.LoginOverlayDisplayed);
+ showMobileDisclaimer = config.GetBindable(OsuSetting.ShowMobileDisclaimer);
if (host.CanExit)
{
@@ -255,6 +260,9 @@ namespace osu.Game.Screens.Menu
[CanBeNull]
private Drawable proxiedLogo;
+ [CanBeNull]
+ private ScheduledDelegate mobileDisclaimerSchedule;
+
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
@@ -275,26 +283,46 @@ namespace osu.Game.Screens.Menu
sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint);
}
- else if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth)
+ else
{
// copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop.
var previousAction = logo.Action;
- // we want to hook into logo.Action to display the login overlay, but also preserve the return value of the old action.
+ // we want to hook into logo.Action to display certain overlays, but also preserve the return value of the old action.
// therefore pass the old action to displayLogin, so that it can return that value.
// this ensures that the OsuLogo sample does not play when it is not desired.
- logo.Action = () => displayLogin(previousAction);
+ logo.Action = () => onLogoClick(previousAction);
}
+ }
- bool displayLogin(Func originalAction)
+ private bool onLogoClick(Func originalAction)
+ {
+ if (showMobileDisclaimer.Value)
{
- if (!loginDisplayed.Value)
+ mobileDisclaimerSchedule?.Cancel();
+ mobileDisclaimerSchedule = Scheduler.AddDelayed(() =>
{
- Scheduler.AddDelayed(() => login?.Show(), 500);
- loginDisplayed.Value = true;
- }
+ dialogOverlay.Push(new MobileDisclaimerDialog(() =>
+ {
+ showMobileDisclaimer.Value = false;
+ displayLoginIfApplicable();
+ }));
+ }, 500);
+ }
+ else
+ displayLoginIfApplicable();
- return originalAction.Invoke();
+ return originalAction.Invoke();
+ }
+
+ private void displayLoginIfApplicable()
+ {
+ if (loginDisplayed.Value) return;
+
+ if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth)
+ {
+ Scheduler.AddDelayed(() => login?.Show(), 500);
+ loginDisplayed.Value = true;
}
}
@@ -443,5 +471,25 @@ namespace osu.Game.Screens.Menu
public void OnReleased(KeyBindingReleaseEvent e)
{
}
+
+ private partial class MobileDisclaimerDialog : PopupDialog
+ {
+ public MobileDisclaimerDialog(Action confirmed)
+ {
+ HeaderText = ButtonSystemStrings.MobileDisclaimerHeader;
+ BodyText = ButtonSystemStrings.MobileDisclaimerBody;
+
+ Icon = FontAwesome.Solid.SmileBeam;
+
+ Buttons = new PopupDialogButton[]
+ {
+ new PopupDialogOkButton
+ {
+ Text = "Understood",
+ Action = confirmed,
+ },
+ };
+ }
+ }
}
}
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/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 79617f172c..1372054149 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -568,6 +568,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Description("Off")]
Off = 0,
+ [Description("10 seconds")]
+ Seconds10 = 10,
+
[Description("30 seconds")]
Seconds30 = 30,
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 837974a8f2..06086c1004 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -29,7 +29,6 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Volume;
using osu.Game.Performance;
-using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Skinning;
@@ -80,8 +79,6 @@ namespace osu.Game.Screens.Play
private FillFlowContainer disclaimers = null!;
private OsuScrollContainer settingsScroll = null!;
- private Bindable lastScore = null!;
-
private Bindable showStoryboards = null!;
private bool backgroundBrightnessReduction;
@@ -183,8 +180,6 @@ namespace osu.Game.Screens.Play
{
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce);
- lastScore = sessionStatics.GetBindable(Static.LastLocalUserScore);
-
showStoryboards = config.GetBindable(OsuSetting.ShowStoryboard);
const float padding = 25;
@@ -354,8 +349,6 @@ namespace osu.Game.Screens.Play
highPerformanceSession?.Dispose();
highPerformanceSession = null;
- lastScore.Value = null;
-
return base.OnExiting(e);
}
diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
index 74b887481f..f93fa1b3c5 100644
--- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
@@ -15,6 +15,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -36,6 +37,8 @@ namespace osu.Game.Screens.Play.PlayerSettings
{
public Bindable ReferenceScore { get; } = new Bindable();
+ private Bindable lastAppliedScore { get; } = new Bindable();
+
public BindableDouble Current { get; } = new BindableDouble
{
MinValue = -50,
@@ -100,6 +103,12 @@ namespace osu.Game.Screens.Play.PlayerSettings
};
}
+ [BackgroundDependencyLoader]
+ private void load(SessionStatics statics)
+ {
+ statics.BindWith(Static.LastAppliedOffsetScore, lastAppliedScore);
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -176,6 +185,9 @@ namespace osu.Game.Screens.Play.PlayerSettings
if (score.NewValue == null)
return;
+ if (score.NewValue.Equals(lastAppliedScore.Value))
+ return;
+
if (!score.NewValue.BeatmapInfo.AsNonNull().Equals(beatmap.Value.BeatmapInfo))
return;
@@ -230,7 +242,11 @@ namespace osu.Game.Screens.Play.PlayerSettings
useAverageButton = new SettingsButton
{
Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay,
- Action = () => Current.Value = lastPlayBeatmapOffset - lastPlayAverage,
+ Action = () =>
+ {
+ Current.Value = lastPlayBeatmapOffset - lastPlayAverage;
+ lastAppliedScore.Value = ReferenceScore.Value;
+ },
Enabled = { Value = !Precision.AlmostEquals(lastPlayAverage, 0, Current.Precision / 2) }
},
globalOffsetText = new LinkFlowContainer
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
-
+
-
+
diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist
index 29410938a3..70747fc9c8 100644
--- a/osu.iOS/Info.plist
+++ b/osu.iOS/Info.plist
@@ -157,5 +157,9 @@
public.app-category.music-games
LSSupportsOpeningDocumentsInPlace
+
+ GCSupportsGameMode
+