From e803b0215f146e449f12a77fae1f09db4fdae30f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 30 Dec 2023 01:38:08 +0100 Subject: [PATCH 001/308] flip along grid axis --- .../Edit/OsuSelectionHandler.cs | 19 ++++++++++--------- osu.Game/Utils/GeometryUtils.cs | 11 +++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index cea2adc6e2..021c735ebd 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -15,7 +16,6 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Utils; using osuTK; @@ -28,6 +28,9 @@ namespace osu.Game.Rulesets.Osu.Edit [Resolved(CanBeNull = true)] private IDistanceSnapProvider? snapProvider { get; set; } + [Resolved] + private OsuGridToolboxGroup gridToolbox { get; set; } = null!; + /// /// During a transform, the initial path types of a single selected slider are stored so they /// can be maintained throughout the operation. @@ -104,13 +107,16 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; - var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects); + var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects); + var flipAxis = flipOverOrigin ? new Vector2(MathF.Cos(MathHelper.DegreesToRadians(gridToolbox.GridLinesRotation.Value)), MathF.Sin(MathHelper.DegreesToRadians(gridToolbox.GridLinesRotation.Value))) : Vector2.UnitX; + flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; + var controlPointFlipQuad = new Quad(); bool didFlip = false; foreach (var h in hitObjects) { - var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position); + var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position); if (!Precision.AlmostEquals(flippedPosition, h.Position)) { @@ -123,12 +129,7 @@ namespace osu.Game.Rulesets.Osu.Edit didFlip = true; foreach (var cp in slider.Path.ControlPoints) - { - cp.Position = new Vector2( - (direction == Direction.Horizontal ? -1 : 1) * cp.Position.X, - (direction == Direction.Vertical ? -1 : 1) * cp.Position.Y - ); - } + cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position); } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index e0d217dd48..aacf9b91f9 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -70,6 +70,17 @@ namespace osu.Game.Utils return position; } + /// + /// Given a flip axis vector, a surrounding quad for all selected objects, and a position, + /// will return the flipped position in screen space coordinates. + /// + public static Vector2 GetFlippedPosition(Vector2 axis, Quad quad, Vector2 position) + { + var centre = quad.Centre; + + return position - 2 * Vector2.Dot(position - centre, axis) * axis; + } + /// /// Given a scale vector, a surrounding quad for all selected objects, and a position, /// will return the scaled position in screen space coordinates. From 078fe5a78ca5b0744d12c6f4fa8f242a0730986c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 30 Dec 2023 01:53:19 +0100 Subject: [PATCH 002/308] Rotate popover rotates around grid center --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 8 +++++--- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 84d5adbc52..02e98d75a7 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit RightToolbox.AddRange(new EditorToolboxGroup[] { OsuGridToolboxGroup, - new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, + new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, GridToolbox = OsuGridToolboxGroup }, FreehandlSliderToolboxGroup } ); diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index f09d6b78e6..02a8ff5872 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -19,14 +18,17 @@ namespace osu.Game.Rulesets.Osu.Edit { private readonly SelectionRotationHandler rotationHandler; + private readonly OsuGridToolboxGroup gridToolbox; + private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre)); private SliderWithTextBoxInput angleInput = null!; private EditorRadioButtonCollection rotationOrigin = null!; - public PreciseRotationPopover(SelectionRotationHandler rotationHandler) + public PreciseRotationPopover(SelectionRotationHandler rotationHandler, OsuGridToolboxGroup gridToolbox) { this.rotationHandler = rotationHandler; + this.gridToolbox = gridToolbox; AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; } @@ -78,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { - rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null); + rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null); }); } diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 3da9f5b69b..f8df45f545 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Edit public SelectionRotationHandler RotationHandler { get; init; } = null!; + public OsuGridToolboxGroup GridToolbox { get; init; } = null!; + public TransformToolboxGroup() : base("transform") { @@ -41,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit { rotateButton = new EditorToolButton("Rotate", () => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, - () => new PreciseRotationPopover(RotationHandler)), + () => new PreciseRotationPopover(RotationHandler, GridToolbox)), // TODO: scale } }; From 09852bc46b6b0b7a78914e19ba3414efab60afdd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 31 Dec 2023 21:23:13 +0100 Subject: [PATCH 003/308] fix horizontal vs vertical flips being mixed up when rotation angle is too big --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 021c735ebd..1cb206c2f8 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -107,9 +106,13 @@ namespace osu.Game.Rulesets.Osu.Edit { var hitObjects = selectedMovableObjects; + // If we're flipping over the origin, we take the grid origin position from the grid toolbox. var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects); - var flipAxis = flipOverOrigin ? new Vector2(MathF.Cos(MathHelper.DegreesToRadians(gridToolbox.GridLinesRotation.Value)), MathF.Sin(MathHelper.DegreesToRadians(gridToolbox.GridLinesRotation.Value))) : Vector2.UnitX; + // If we're flipping over the origin, we take the grid rotation from the grid toolbox. + // We want to normalize the rotation angle to -45 to 45 degrees, so horizontal vs vertical flip is not mixed up by the rotation and it stays intuitive to use. + var flipAxis = flipOverOrigin ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)) : Vector2.UnitX; flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; + var controlPointFlipQuad = new Quad(); bool didFlip = false; From b5dbf24d2787569b4f813bf5631507492cdb0269 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 1 Feb 2024 10:19:09 -0500 Subject: [PATCH 004/308] early replay analysis settings version committing early version for others in the discussion to do their own testing with it --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 3 + .../UI/HitMarkerContainer.cs | 134 ++++++++++++++++++ .../UI/OsuAnalysisSettings.cs | 81 +++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 + .../PlayerSettingsOverlayStrings.cs | 15 ++ osu.Game/Rulesets/Ruleset.cs | 3 + osu.Game/Screens/Play/Player.cs | 2 +- .../Play/PlayerSettings/AnalysisSettings.cs | 18 +++ osu.Game/Screens/Play/ReplayPlayer.cs | 5 + 9 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs create mode 100644 osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs create mode 100644 osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 6752712be1..358553ac59 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -38,6 +38,7 @@ using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; @@ -356,5 +357,7 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } + + public override AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => new OsuAnalysisSettings(drawableRuleset); } } diff --git a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs new file mode 100644 index 0000000000..a9fc42e596 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs @@ -0,0 +1,134 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI +{ + public partial class HitMarkerContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler + { + private Vector2 lastMousePosition; + + public Bindable HitMarkerEnabled = new BindableBool(); + public Bindable AimMarkersEnabled = new BindableBool(); + + public override bool ReceivePositionalInputAt(Vector2 _) => true; + + public bool OnPressed(KeyBindingPressEvent e) + { + if (HitMarkerEnabled.Value && (e.Action == OsuAction.LeftButton || e.Action == OsuAction.RightButton)) + { + AddMarker(e.Action); + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) { } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + lastMousePosition = e.MousePosition; + + if (AimMarkersEnabled.Value) + { + AddMarker(null); + } + + return base.OnMouseMove(e); + } + + private void AddMarker(OsuAction? action) + { + Add(new HitMarkerDrawable(action) { Position = lastMousePosition }); + } + + private partial class HitMarkerDrawable : CompositeDrawable + { + private const double lifetime_duration = 1000; + private const double fade_out_time = 400; + + public override bool RemoveWhenNotAlive => true; + + public HitMarkerDrawable(OsuAction? action) + { + var colour = Colour4.Gray.Opacity(0.5F); + var length = 8; + var depth = float.MaxValue; + switch (action) + { + case OsuAction.LeftButton: + colour = Colour4.Orange; + length = 20; + depth = float.MinValue; + break; + case OsuAction.RightButton: + colour = Colour4.LightGreen; + length = 20; + depth = float.MinValue; + break; + } + + this.Depth = depth; + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 45, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 135, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 45, + Colour = colour + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 135, + Colour = colour + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LifetimeStart = Time.Current; + LifetimeEnd = LifetimeStart + lifetime_duration; + + Scheduler.AddDelayed(() => + { + this.FadeOut(fade_out_time); + }, lifetime_duration - fade_out_time); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs new file mode 100644 index 0000000000..4694c1a560 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Localisation; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play.PlayerSettings; + +namespace osu.Game.Rulesets.Osu.UI +{ + public partial class OsuAnalysisSettings : AnalysisSettings + { + private static readonly Logger logger = Logger.GetLogger("osu-analysis-settings"); + + protected new DrawableOsuRuleset drawableRuleset => (DrawableOsuRuleset)base.drawableRuleset; + + private readonly PlayerCheckbox hitMarkerToggle; + private readonly PlayerCheckbox aimMarkerToggle; + private readonly PlayerCheckbox hideCursorToggle; + private readonly PlayerCheckbox? hiddenToggle; + + public OsuAnalysisSettings(DrawableRuleset drawableRuleset) + : base(drawableRuleset) + { + Children = new Drawable[] + { + hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, + aimMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimMarkers }, + hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } + }; + + // hidden stuff is just here for testing at the moment; to create the mod disabling functionality + + foreach (var mod in drawableRuleset.Mods) + { + if (mod is OsuModHidden) + { + logger.Add("Hidden is enabled", LogLevel.Debug); + Add(hiddenToggle = new PlayerCheckbox { LabelText = "Disable hidden" }); + break; + } + } + } + + protected override void LoadComplete() + { + drawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); + drawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); + hideCursorToggle.Current.BindValueChanged(onCursorToggle); + hiddenToggle?.Current.BindValueChanged(onHiddenToggle); + } + + private void onCursorToggle(ValueChangedEvent hide) + { + // this only hides half the cursor + if (hide.NewValue) + { + drawableRuleset.Playfield.Cursor.Hide(); + } else + { + drawableRuleset.Playfield.Cursor.Show(); + } + } + + private void onHiddenToggle(ValueChangedEvent off) + { + if (off.NewValue) + { + logger.Add("Hidden off", LogLevel.Debug); + } else + { + logger.Add("Hidden on", LogLevel.Debug); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 411a02c5af..d7e1732175 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.UI private readonly JudgementPooler judgementPooler; public SmokeContainer Smoke { get; } + + public HitMarkerContainer MarkersContainer { get; } + public FollowPointRenderer FollowPoints { get; } public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -59,6 +62,7 @@ namespace osu.Game.Rulesets.Osu.UI HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, + MarkersContainer = new HitMarkerContainer { RelativeSizeAxes = Axes.Both } }; HitPolicy = new StartTimeOrderedHitPolicy(); diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index 60874da561..f829fb4ac1 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -19,6 +19,21 @@ namespace osu.Game.Localisation /// public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame"); + /// + /// "Hit markers" + /// + public static LocalisableString HitMarkers => new TranslatableString(getKey(@"hit_markers"), @"Hit markers"); + + /// + /// "Aim markers" + /// + public static LocalisableString AimMarkers => new TranslatableString(getKey(@"aim_markers"), @"Aim markers"); + + /// + /// "Hide cursor" + /// + public static LocalisableString HideCursor => new TranslatableString(getKey(@"hide_cursor"), @"Hide cursor"); + /// /// "Seek backward {0} seconds" /// diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 37a35fd3ae..5fbb23f094 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -27,6 +27,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; using osu.Game.Users; @@ -402,5 +403,7 @@ namespace osu.Game.Rulesets /// Can be overridden to alter the difficulty section to the editor beatmap setup screen. /// public virtual DifficultySection? CreateEditorDifficultySection() => null; + + public virtual AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => null; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ad1f9ec897..0fa7143693 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Play public GameplayState GameplayState { get; private set; } - private Ruleset ruleset; + protected Ruleset ruleset; public BreakOverlay BreakOverlay; diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs new file mode 100644 index 0000000000..122ca29142 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.UI; + +namespace osu.Game.Screens.Play.PlayerSettings +{ + public partial class AnalysisSettings : PlayerSettingsGroup + { + protected DrawableRuleset drawableRuleset; + + public AnalysisSettings(DrawableRuleset drawableRuleset) + : base("Analysis Settings") + { + this.drawableRuleset = drawableRuleset; + } + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 3c5b85662a..0d877785e7 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -71,6 +71,11 @@ namespace osu.Game.Screens.Play playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); + + var analysisSettings = ruleset.CreateAnalysisSettings(DrawableRuleset); + if (analysisSettings != null) { + HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); + } } protected override void PrepareReplay() From 236f029dad22722ebf73c02d40dc19b312b16642 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 16:56:57 +0100 Subject: [PATCH 005/308] Remove Masking from PositionSnapGrid This caused issues in rendering the outline of the grid because the outline was getting masked at some resolutions. --- .../Components/LinedPositionSnapGrid.cs | 128 +++++++++++++++--- .../Compose/Components/PositionSnapGrid.cs | 2 - 2 files changed, 106 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs index ebdd76a4e2..8a7f6b5344 100644 --- a/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/LinedPositionSnapGrid.cs @@ -15,18 +15,29 @@ namespace osu.Game.Screens.Edit.Compose.Components { protected void GenerateGridLines(Vector2 step, Vector2 drawSize) { + if (Precision.AlmostEquals(step, Vector2.Zero)) + return; + int index = 0; - var currentPosition = StartPosition.Value; // Make lines the same width independent of display resolution. float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width; - float lineLength = drawSize.Length * 2; + float rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)); List generatedLines = new List(); - while (lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize) || - isMovingTowardsBox(currentPosition, step, drawSize)) + while (true) { + Vector2 currentPosition = StartPosition.Value + index++ * step; + + if (!lineDefinitelyIntersectsBox(currentPosition, step.PerpendicularLeft, drawSize, out var p1, out var p2)) + { + if (!isMovingTowardsBox(currentPosition, step, drawSize)) + break; + + continue; + } + var gridLine = new Box { Colour = Colour4.White, @@ -34,15 +45,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Origin = Anchor.Centre, RelativeSizeAxes = Axes.None, Width = lineWidth, - Height = lineLength, - Position = currentPosition, - Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(step.Y, step.X)), + Height = Vector2.Distance(p1, p2), + Position = (p1 + p2) / 2, + Rotation = rotation, }; generatedLines.Add(gridLine); - - index += 1; - currentPosition = StartPosition.Value + index * step; } if (generatedLines.Count == 0) @@ -59,23 +67,99 @@ namespace osu.Game.Screens.Edit.Compose.Components (currentPosition + step - box).LengthSquared < (currentPosition - box).LengthSquared; } - private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box) + /// + /// Determines if the line starting at and going in the direction of + /// definitely intersects the box on (0, 0) with the given width and height and returns the intersection points if it does. + /// + /// The start point of the line. + /// The direction of the line. + /// The width and height of the box. + /// The first intersection point. + /// The second intersection point. + /// Whether the line definitely intersects the box. + private bool lineDefinitelyIntersectsBox(Vector2 lineStart, Vector2 lineDir, Vector2 box, out Vector2 p1, out Vector2 p2) { - var p2 = lineStart + lineDir; + p1 = Vector2.Zero; + p2 = Vector2.Zero; - double d1 = det(Vector2.Zero); - double d2 = det(new Vector2(box.X, 0)); - double d3 = det(new Vector2(0, box.Y)); - double d4 = det(box); + if (Precision.AlmostEquals(lineDir.X, 0)) + { + // If the line is vertical, we only need to check if the X coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.X, 0) || !Precision.DefinitelyBigger(box.X, lineStart.X)) + return false; - return definitelyDifferentSign(d1, d2) || definitelyDifferentSign(d3, d4) || - definitelyDifferentSign(d1, d3) || definitelyDifferentSign(d2, d4); + p1 = new Vector2(lineStart.X, 0); + p2 = new Vector2(lineStart.X, box.Y); + return true; + } - double det(Vector2 p) => (p.X - lineStart.X) * (p2.Y - lineStart.Y) - (p.Y - lineStart.Y) * (p2.X - lineStart.X); + if (Precision.AlmostEquals(lineDir.Y, 0)) + { + // If the line is horizontal, we only need to check if the Y coordinate of the line is within the box. + if (!Precision.DefinitelyBigger(lineStart.Y, 0) || !Precision.DefinitelyBigger(box.Y, lineStart.Y)) + return false; - bool definitelyDifferentSign(double a, double b) => !Precision.AlmostEquals(a, 0) && - !Precision.AlmostEquals(b, 0) && - Math.Sign(a) != Math.Sign(b); + p1 = new Vector2(0, lineStart.Y); + p2 = new Vector2(box.X, lineStart.Y); + return true; + } + + float m = lineDir.Y / lineDir.X; + float mInv = lineDir.X / lineDir.Y; // Use this to improve numerical stability if X is close to zero. + float b = lineStart.Y - m * lineStart.X; + + // Calculate intersection points with the sides of the box. + var p = new List(4); + + if (0 <= b && b <= box.Y) + p.Add(new Vector2(0, b)); + if (0 <= (box.Y - b) * mInv && (box.Y - b) * mInv <= box.X) + p.Add(new Vector2((box.Y - b) * mInv, box.Y)); + if (0 <= m * box.X + b && m * box.X + b <= box.Y) + p.Add(new Vector2(box.X, m * box.X + b)); + if (0 <= -b * mInv && -b * mInv <= box.X) + p.Add(new Vector2(-b * mInv, 0)); + + switch (p.Count) + { + case 4: + // If there are 4 intersection points, the line is a diagonal of the box. + if (m > 0) + { + p1 = Vector2.Zero; + p2 = box; + } + else + { + p1 = new Vector2(0, box.Y); + p2 = new Vector2(box.X, 0); + } + + break; + + case 3: + // If there are 3 intersection points, the line goes through a corner of the box. + if (p[0] == p[1]) + { + p1 = p[0]; + p2 = p[2]; + } + else + { + p1 = p[0]; + p2 = p[1]; + } + + break; + + case 2: + p1 = p[0]; + p2 = p[1]; + + break; + } + + return !Precision.AlmostEquals(p1, p2); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs index 36687ef73a..e576ac1e49 100644 --- a/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/PositionSnapGrid.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected PositionSnapGrid() { - Masking = true; - StartPosition.BindValueChanged(_ => GridCache.Invalidate()); AddLayout(GridCache); From b4b5cdfcf2d84527ec4d5cb9e504152be59b7e19 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Thu, 1 Feb 2024 17:07:03 +0100 Subject: [PATCH 006/308] Fix masking in circular snap grid --- .../Edit/Compose/Components/CircularPositionSnapGrid.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs index 403a270359..791cb33439 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularPositionSnapGrid.cs @@ -82,7 +82,12 @@ namespace osu.Game.Screens.Edit.Compose.Components generatedCircles.First().Alpha = 0.8f; - AddRangeInternal(generatedCircles); + AddInternal(new Container + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Children = generatedCircles, + }); } public override Vector2 GetSnappedPosition(Vector2 original) From 288eed53df439c594ffab544d3b83d9b2ea9d343 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Sat, 10 Feb 2024 02:02:26 -0500 Subject: [PATCH 007/308] new features + improvements Hit & aim markers are skinnable. Hidden can be toggled off. Aim line with skinnable color was added. The fadeout time is based on the approach rate. Cursor hide fixed. --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 77 ++++++- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 12 +- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 3 + .../Skinning/Default/DefaultHitMarker.cs | 71 +++++++ .../Skinning/Default/HitMarker.cs | 33 +++ .../Legacy/OsuLegacySkinTransformer.cs | 19 ++ .../Skinning/OsuSkinColour.cs | 1 + .../UI/HitMarkerContainer.cs | 188 ++++++++++++------ .../UI/OsuAnalysisSettings.cs | 68 +++++-- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- .../PlayerSettingsOverlayStrings.cs | 5 + .../Rulesets/Mods/IToggleableVisibility.cs | 11 + 12 files changed, 393 insertions(+), 97 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs create mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs create mode 100644 osu.Game/Rulesets/Mods/IToggleableVisibility.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 6dc0d5d522..a69bed6fb2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -12,13 +14,14 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden, IHidesApproachCircles + public class OsuModHidden : ModHidden, IHidesApproachCircles, IToggleableVisibility { [SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")] public Bindable OnlyFadeApproachCircles { get; } = new BindableBool(); @@ -28,24 +31,41 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) }; + private bool toggledOff = false; + private IBeatmap? appliedBeatmap; + public const double FADE_IN_DURATION_MULTIPLIER = 0.4; public const double FADE_OUT_DURATION_MULTIPLIER = 0.3; protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); - public override void ApplyToBeatmap(IBeatmap beatmap) + public override void ApplyToBeatmap(IBeatmap? beatmap = null) { + if (beatmap is not null) + appliedBeatmap = beatmap; + else if (appliedBeatmap is null) + return; + else + beatmap = appliedBeatmap; + base.ApplyToBeatmap(beatmap); foreach (var obj in beatmap.HitObjects.OfType()) applyFadeInAdjustment(obj); + } - static void applyFadeInAdjustment(OsuHitObject osuObject) - { - osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER; - foreach (var nested in osuObject.NestedHitObjects.OfType()) - applyFadeInAdjustment(nested); - } + private static void applyFadeInAdjustment(OsuHitObject osuObject) + { + osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER; + foreach (var nested in osuObject.NestedHitObjects.OfType()) + applyFadeInAdjustment(nested); + } + + private static void revertFadeInAdjustment(OsuHitObject osuObject) + { + osuObject.TimeFadeIn = OsuHitObject.CalculateTimeFadeIn(osuObject.TimePreempt); + foreach (var nested in osuObject.NestedHitObjects.OfType()) + revertFadeInAdjustment(nested); } protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -60,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Mods private void applyHiddenState(DrawableHitObject drawableObject, bool increaseVisibility) { - if (!(drawableObject is DrawableOsuHitObject drawableOsuObject)) + if (!(drawableObject is DrawableOsuHitObject drawableOsuObject) || toggledOff) return; OsuHitObject hitObject = drawableOsuObject.HitObject; @@ -183,8 +203,11 @@ namespace osu.Game.Rulesets.Osu.Mods } } - private static void hideSpinnerApproachCircle(DrawableSpinner spinner) + private void hideSpinnerApproachCircle(DrawableSpinner spinner) { + if (toggledOff) + return; + var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle; if (approachCircle == null) return; @@ -192,5 +215,39 @@ namespace osu.Game.Rulesets.Osu.Mods using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt)) approachCircle.Hide(); } + + public void ToggleOffVisibility(Playfield playfield) + { + if (toggledOff) + return; + + toggledOff = true; + + if (appliedBeatmap is not null) + foreach (var obj in appliedBeatmap.HitObjects.OfType()) + revertFadeInAdjustment(obj); + + foreach (var dho in playfield.AllHitObjects) + { + dho.RefreshStateTransforms(); + } + } + + public void ToggleOnVisibility(Playfield playfield) + { + if (!toggledOff) + return; + + toggledOff = false; + + if (appliedBeatmap is not null) + ApplyToBeatmap(); + + foreach (var dho in playfield.AllHitObjects) + { + dho.RefreshStateTransforms(); + ApplyToDrawableHitObject(dho); + } + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 74631400ca..60305ed953 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -154,17 +154,19 @@ namespace osu.Game.Rulesets.Osu.Objects }); } + // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. + // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. + // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. + // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. + static public double CalculateTimeFadeIn(double timePreempt) => 400 * Math.Min(1, timePreempt / PREEMPT_MIN); + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN); - // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. - // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. - // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. - // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. - TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN); + TimeFadeIn = CalculateTimeFadeIn(TimePreempt); Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize, true); } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 52fdfea95f..75db18d345 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -22,5 +22,8 @@ namespace osu.Game.Rulesets.Osu SpinnerBody, CursorSmoke, ApproachCircle, + HitMarkerLeft, + HitMarkerRight, + AimMarker } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs new file mode 100644 index 0000000000..f4c11e6c8a --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs @@ -0,0 +1,71 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public partial class DefaultHitMarker : CompositeDrawable + { + public DefaultHitMarker(OsuAction? action) + { + var (colour, length, hasBorder) = getConfig(action); + + if (hasBorder) + { + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 45, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 135, + Colour = Colour4.Black.Opacity(0.5F) + } + }; + } + + AddRangeInternal(new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 45, + Colour = colour + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 135, + Colour = colour + } + }); + } + + private (Colour4 colour, float length, bool hasBorder) getConfig(OsuAction? action) + { + switch (action) + { + case OsuAction.LeftButton: + return (Colour4.Orange, 20, true); + case OsuAction.RightButton: + return (Colour4.LightGreen, 20, true); + default: + return (Colour4.Gray.Opacity(0.3F), 8, false); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs new file mode 100644 index 0000000000..d86e3d4f79 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs @@ -0,0 +1,33 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public partial class HitMarker : Sprite + { + private readonly OsuAction? action; + + public HitMarker(OsuAction? action = null) + { + this.action = action; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + switch (action) + { + case OsuAction.LeftButton: + Texture = skin.GetTexture(@"hitmarker-left"); + break; + case OsuAction.RightButton: + Texture = skin.GetTexture(@"hitmarker-right"); + break; + default: + Texture = skin.GetTexture(@"aimmarker"); + break; + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index d2ebc68c52..84c055fe5e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; @@ -168,6 +169,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; + case OsuSkinComponents.HitMarkerLeft: + if (GetTexture(@"hitmarker-left") != null) + return new HitMarker(OsuAction.LeftButton); + + return null; + + case OsuSkinComponents.HitMarkerRight: + if (GetTexture(@"hitmarker-right") != null) + return new HitMarker(OsuAction.RightButton); + + return null; + + case OsuSkinComponents.AimMarker: + if (GetTexture(@"aimmarker") != null) + return new HitMarker(); + + return null; + default: throw new UnsupportedSkinComponentException(lookup); } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs index 24f9217a5f..5c864fb6c2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs @@ -10,5 +10,6 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderBall, SpinnerBackground, StarBreakAdditive, + ReplayAimLine, } } diff --git a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs index a9fc42e596..01877a9185 100644 --- a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs @@ -10,111 +10,128 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI { public partial class HitMarkerContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler { private Vector2 lastMousePosition; + private Vector2? lastlastMousePosition; + private double? timePreempt; + private double TimePreempt + { + get => timePreempt ?? default_time_preempt; + set => timePreempt = value; + } public Bindable HitMarkerEnabled = new BindableBool(); public Bindable AimMarkersEnabled = new BindableBool(); + public Bindable AimLinesEnabled = new BindableBool(); + + private const double default_time_preempt = 1000; + + private readonly HitObjectContainer hitObjectContainer; public override bool ReceivePositionalInputAt(Vector2 _) => true; + public HitMarkerContainer(HitObjectContainer hitObjectContainer) + { + this.hitObjectContainer = hitObjectContainer; + } + public bool OnPressed(KeyBindingPressEvent e) { if (HitMarkerEnabled.Value && (e.Action == OsuAction.LeftButton || e.Action == OsuAction.RightButton)) { + updateTimePreempt(); AddMarker(e.Action); } return false; } - public void OnReleased(KeyBindingReleaseEvent e) { } + public void OnReleased(KeyBindingReleaseEvent e) + { + } protected override bool OnMouseMove(MouseMoveEvent e) { + lastlastMousePosition = lastMousePosition; lastMousePosition = e.MousePosition; if (AimMarkersEnabled.Value) { + updateTimePreempt(); AddMarker(null); } + if (AimLinesEnabled.Value && lastlastMousePosition != null && lastlastMousePosition != lastMousePosition) + { + if (!AimMarkersEnabled.Value) + updateTimePreempt(); + Add(new AimLineDrawable((Vector2)lastlastMousePosition, lastMousePosition, TimePreempt)); + } + return base.OnMouseMove(e); } private void AddMarker(OsuAction? action) { - Add(new HitMarkerDrawable(action) { Position = lastMousePosition }); + var component = OsuSkinComponents.AimMarker; + switch(action) + { + case OsuAction.LeftButton: + component = OsuSkinComponents.HitMarkerLeft; + break; + case OsuAction.RightButton: + component = OsuSkinComponents.HitMarkerRight; + break; + } + + Add(new HitMarkerDrawable(action, component, TimePreempt) + { + Position = lastMousePosition, + Origin = Anchor.Centre, + Depth = action == null ? float.MaxValue : float.MinValue + }); } - private partial class HitMarkerDrawable : CompositeDrawable + private void updateTimePreempt() { - private const double lifetime_duration = 1000; - private const double fade_out_time = 400; + var hitObject = getHitObject(); + if (hitObject == null) + return; + + TimePreempt = hitObject.TimePreempt; + } + + private OsuHitObject? getHitObject() + { + foreach (var dho in hitObjectContainer.Objects) + return (dho as DrawableOsuHitObject)?.HitObject; + return null; + } + + private partial class HitMarkerDrawable : SkinnableDrawable + { + private readonly double lifetimeDuration; + private readonly double fadeOutTime; public override bool RemoveWhenNotAlive => true; - public HitMarkerDrawable(OsuAction? action) + public HitMarkerDrawable(OsuAction? action, OsuSkinComponents componenet, double timePreempt) + : base(new OsuSkinComponentLookup(componenet), _ => new DefaultHitMarker(action)) { - var colour = Colour4.Gray.Opacity(0.5F); - var length = 8; - var depth = float.MaxValue; - switch (action) - { - case OsuAction.LeftButton: - colour = Colour4.Orange; - length = 20; - depth = float.MinValue; - break; - case OsuAction.RightButton: - colour = Colour4.LightGreen; - length = 20; - depth = float.MinValue; - break; - } - - this.Depth = depth; - - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 45, - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 135, - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 45, - Colour = colour - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 135, - Colour = colour - } - }; + fadeOutTime = timePreempt / 2; + lifetimeDuration = timePreempt + fadeOutTime; } protected override void LoadComplete() @@ -122,12 +139,57 @@ namespace osu.Game.Rulesets.Osu.UI base.LoadComplete(); LifetimeStart = Time.Current; - LifetimeEnd = LifetimeStart + lifetime_duration; + LifetimeEnd = LifetimeStart + lifetimeDuration; Scheduler.AddDelayed(() => { - this.FadeOut(fade_out_time); - }, lifetime_duration - fade_out_time); + this.FadeOut(fadeOutTime); + }, lifetimeDuration - fadeOutTime); + } + } + + private partial class AimLineDrawable : CompositeDrawable + { + private readonly double lifetimeDuration; + private readonly double fadeOutTime; + + public override bool RemoveWhenNotAlive => true; + + public AimLineDrawable(Vector2 fromP, Vector2 toP, double timePreempt) + { + fadeOutTime = timePreempt / 2; + lifetimeDuration = timePreempt + fadeOutTime; + + float distance = Vector2.Distance(fromP, toP); + Vector2 direction = (toP - fromP); + InternalChild = new Box + { + Position = fromP + (direction / 2), + Size = new Vector2(distance, 1), + Rotation = (float)(Math.Atan(direction.Y / direction.X) * (180 / Math.PI)), + Origin = Anchor.Centre + }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + var color = skin.GetConfig(OsuSkinColour.ReplayAimLine)?.Value ?? Color4.White; + color.A = 127; + InternalChild.Colour = color; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LifetimeStart = Time.Current; + LifetimeEnd = LifetimeStart + lifetimeDuration; + + Scheduler.AddDelayed(() => + { + this.FadeOut(fadeOutTime); + }, lifetimeDuration - fadeOutTime); } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 4694c1a560..050675d970 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -4,7 +4,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Logging; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -15,14 +17,13 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisSettings : AnalysisSettings { - private static readonly Logger logger = Logger.GetLogger("osu-analysis-settings"); - protected new DrawableOsuRuleset drawableRuleset => (DrawableOsuRuleset)base.drawableRuleset; private readonly PlayerCheckbox hitMarkerToggle; private readonly PlayerCheckbox aimMarkerToggle; private readonly PlayerCheckbox hideCursorToggle; - private readonly PlayerCheckbox? hiddenToggle; + private readonly PlayerCheckbox aimLinesToggle; + private readonly FillFlowContainer modTogglesContainer; public OsuAnalysisSettings(DrawableRuleset drawableRuleset) : base(drawableRuleset) @@ -31,18 +32,36 @@ namespace osu.Game.Rulesets.Osu.UI { hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, aimMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimMarkers }, - hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } + aimLinesToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimLines }, + hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor }, + new OsuScrollContainer(Direction.Horizontal) + { + RelativeSizeAxes = Axes.X, + Height = ModSwitchSmall.DEFAULT_SIZE, + ScrollbarOverlapsContent = false, + Child = modTogglesContainer = new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + } + } }; - - // hidden stuff is just here for testing at the moment; to create the mod disabling functionality foreach (var mod in drawableRuleset.Mods) { - if (mod is OsuModHidden) + if (mod is IToggleableVisibility toggleableMod) { - logger.Add("Hidden is enabled", LogLevel.Debug); - Add(hiddenToggle = new PlayerCheckbox { LabelText = "Disable hidden" }); - break; + var modSwitch = new SelectableModSwitchSmall(mod) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Active = { Value = true } + }; + modSwitch.Active.BindValueChanged((v) => onModToggle(toggleableMod, v)); + modTogglesContainer.Add(modSwitch); } } } @@ -51,8 +70,8 @@ namespace osu.Game.Rulesets.Osu.UI { drawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); drawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); + drawableRuleset.Playfield.MarkersContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); hideCursorToggle.Current.BindValueChanged(onCursorToggle); - hiddenToggle?.Current.BindValueChanged(onHiddenToggle); } private void onCursorToggle(ValueChangedEvent hide) @@ -60,21 +79,34 @@ namespace osu.Game.Rulesets.Osu.UI // this only hides half the cursor if (hide.NewValue) { - drawableRuleset.Playfield.Cursor.Hide(); + drawableRuleset.Playfield.Cursor.FadeOut(); } else { - drawableRuleset.Playfield.Cursor.Show(); + drawableRuleset.Playfield.Cursor.FadeIn(); } } - private void onHiddenToggle(ValueChangedEvent off) + private void onModToggle(IToggleableVisibility mod, ValueChangedEvent toggled) { - if (off.NewValue) + if (toggled.NewValue) { - logger.Add("Hidden off", LogLevel.Debug); + mod.ToggleOnVisibility(drawableRuleset.Playfield); } else { - logger.Add("Hidden on", LogLevel.Debug); + mod.ToggleOffVisibility(drawableRuleset.Playfield); + } + } + + private partial class SelectableModSwitchSmall : ModSwitchSmall + { + public SelectableModSwitchSmall(IMod mod) + : base(mod) + {} + + protected override bool OnClick(ClickEvent e) + { + Active.Value = !Active.Value; + return true; } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index d7e1732175..3cb3c50ef7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.UI HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, - MarkersContainer = new HitMarkerContainer { RelativeSizeAxes = Axes.Both } + MarkersContainer = new HitMarkerContainer(HitObjectContainer) { RelativeSizeAxes = Axes.Both } }; HitPolicy = new StartTimeOrderedHitPolicy(); diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index f829fb4ac1..017cc9bf82 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -34,6 +34,11 @@ namespace osu.Game.Localisation /// public static LocalisableString HideCursor => new TranslatableString(getKey(@"hide_cursor"), @"Hide cursor"); + /// + /// "Aim lines" + /// + public static LocalisableString AimLines => new TranslatableString(getKey(@"aim_lines"), @"Aim lines"); + /// /// "Seek backward {0} seconds" /// diff --git a/osu.Game/Rulesets/Mods/IToggleableVisibility.cs b/osu.Game/Rulesets/Mods/IToggleableVisibility.cs new file mode 100644 index 0000000000..5d90c24b9e --- /dev/null +++ b/osu.Game/Rulesets/Mods/IToggleableVisibility.cs @@ -0,0 +1,11 @@ +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mods +{ + public interface IToggleableVisibility + { + public void ToggleOffVisibility(Playfield playfield); + + public void ToggleOnVisibility(Playfield playfield); + } +} \ No newline at end of file From 1d552e7c90b8d3f7350fecaa43f5759b0154eaa7 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Tue, 20 Feb 2024 22:28:25 -0500 Subject: [PATCH 008/308] move skin component logic --- .../Default/OsuTrianglesSkinTransformer.cs | 22 +++++++++++++++++++ .../Legacy/OsuLegacySkinTransformer.cs | 22 ------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs index 7a4c768aa2..69ef1d9dc0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs @@ -29,6 +29,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default return new DefaultJudgementPieceSliderTickMiss(result); } + break; + case OsuSkinComponentLookup osuComponent: + switch (osuComponent.Component) + { + case OsuSkinComponents.HitMarkerLeft: + if (GetTexture(@"hitmarker-left") != null) + return new HitMarker(OsuAction.LeftButton); + + return null; + + case OsuSkinComponents.HitMarkerRight: + if (GetTexture(@"hitmarker-right") != null) + return new HitMarker(OsuAction.RightButton); + + return null; + + case OsuSkinComponents.AimMarker: + if (GetTexture(@"aimmarker") != null) + return new HitMarker(); + + return null; + } break; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 84c055fe5e..a931d3ecbf 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; @@ -168,27 +167,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyApproachCircle(); return null; - - case OsuSkinComponents.HitMarkerLeft: - if (GetTexture(@"hitmarker-left") != null) - return new HitMarker(OsuAction.LeftButton); - - return null; - - case OsuSkinComponents.HitMarkerRight: - if (GetTexture(@"hitmarker-right") != null) - return new HitMarker(OsuAction.RightButton); - - return null; - - case OsuSkinComponents.AimMarker: - if (GetTexture(@"aimmarker") != null) - return new HitMarker(); - - return null; - - default: - throw new UnsupportedSkinComponentException(lookup); } } From 35b89966bccc2455034c7aed973cf2fefbf87ca7 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Wed, 21 Feb 2024 23:23:40 -0500 Subject: [PATCH 009/308] revert mod visibility toggle --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 79 +++---------------- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 12 ++- .../UI/OsuAnalysisSettings.cs | 57 +------------ .../Rulesets/Mods/IToggleableVisibility.cs | 11 --- 4 files changed, 17 insertions(+), 142 deletions(-) delete mode 100644 osu.Game/Rulesets/Mods/IToggleableVisibility.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index a69bed6fb2..e45daed919 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -2,11 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; -using osu.Framework.Graphics.Transforms; using osu.Framework.Bindables; using osu.Framework.Localisation; using osu.Game.Configuration; @@ -14,14 +12,13 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden, IHidesApproachCircles, IToggleableVisibility + public class OsuModHidden : ModHidden, IHidesApproachCircles { [SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")] public Bindable OnlyFadeApproachCircles { get; } = new BindableBool(); @@ -31,41 +28,24 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) }; - private bool toggledOff = false; - private IBeatmap? appliedBeatmap; - public const double FADE_IN_DURATION_MULTIPLIER = 0.4; public const double FADE_OUT_DURATION_MULTIPLIER = 0.3; protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); - public override void ApplyToBeatmap(IBeatmap? beatmap = null) + public override void ApplyToBeatmap(IBeatmap beatmap) { - if (beatmap is not null) - appliedBeatmap = beatmap; - else if (appliedBeatmap is null) - return; - else - beatmap = appliedBeatmap; - base.ApplyToBeatmap(beatmap); foreach (var obj in beatmap.HitObjects.OfType()) applyFadeInAdjustment(obj); - } - private static void applyFadeInAdjustment(OsuHitObject osuObject) - { - osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER; - foreach (var nested in osuObject.NestedHitObjects.OfType()) - applyFadeInAdjustment(nested); - } - - private static void revertFadeInAdjustment(OsuHitObject osuObject) - { - osuObject.TimeFadeIn = OsuHitObject.CalculateTimeFadeIn(osuObject.TimePreempt); - foreach (var nested in osuObject.NestedHitObjects.OfType()) - revertFadeInAdjustment(nested); + static void applyFadeInAdjustment(OsuHitObject osuObject) + { + osuObject.TimeFadeIn = osuObject.TimePreempt * FADE_IN_DURATION_MULTIPLIER; + foreach (var nested in osuObject.NestedHitObjects.OfType()) + applyFadeInAdjustment(nested); + } } protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -80,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods private void applyHiddenState(DrawableHitObject drawableObject, bool increaseVisibility) { - if (!(drawableObject is DrawableOsuHitObject drawableOsuObject) || toggledOff) + if (!(drawableObject is DrawableOsuHitObject drawableOsuObject)) return; OsuHitObject hitObject = drawableOsuObject.HitObject; @@ -203,11 +183,8 @@ namespace osu.Game.Rulesets.Osu.Mods } } - private void hideSpinnerApproachCircle(DrawableSpinner spinner) + private static void hideSpinnerApproachCircle(DrawableSpinner spinner) { - if (toggledOff) - return; - var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle; if (approachCircle == null) return; @@ -215,39 +192,5 @@ namespace osu.Game.Rulesets.Osu.Mods using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt)) approachCircle.Hide(); } - - public void ToggleOffVisibility(Playfield playfield) - { - if (toggledOff) - return; - - toggledOff = true; - - if (appliedBeatmap is not null) - foreach (var obj in appliedBeatmap.HitObjects.OfType()) - revertFadeInAdjustment(obj); - - foreach (var dho in playfield.AllHitObjects) - { - dho.RefreshStateTransforms(); - } - } - - public void ToggleOnVisibility(Playfield playfield) - { - if (!toggledOff) - return; - - toggledOff = false; - - if (appliedBeatmap is not null) - ApplyToBeatmap(); - - foreach (var dho in playfield.AllHitObjects) - { - dho.RefreshStateTransforms(); - ApplyToDrawableHitObject(dho); - } - } } -} +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 60305ed953..74631400ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -154,19 +154,17 @@ namespace osu.Game.Rulesets.Osu.Objects }); } - // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. - // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. - // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. - // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. - static public double CalculateTimeFadeIn(double timePreempt) => 400 * Math.Min(1, timePreempt / PREEMPT_MIN); - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN); - TimeFadeIn = CalculateTimeFadeIn(TimePreempt); + // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. + // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. + // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. + // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. + TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN); Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize, true); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 050675d970..319ae3e1f5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osu.Game.Localisation; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.PlayerSettings; @@ -23,7 +22,6 @@ namespace osu.Game.Rulesets.Osu.UI private readonly PlayerCheckbox aimMarkerToggle; private readonly PlayerCheckbox hideCursorToggle; private readonly PlayerCheckbox aimLinesToggle; - private readonly FillFlowContainer modTogglesContainer; public OsuAnalysisSettings(DrawableRuleset drawableRuleset) : base(drawableRuleset) @@ -33,37 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, aimMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimMarkers }, aimLinesToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimLines }, - hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor }, - new OsuScrollContainer(Direction.Horizontal) - { - RelativeSizeAxes = Axes.X, - Height = ModSwitchSmall.DEFAULT_SIZE, - ScrollbarOverlapsContent = false, - Child = modTogglesContainer = new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X - } - } + hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } }; - - foreach (var mod in drawableRuleset.Mods) - { - if (mod is IToggleableVisibility toggleableMod) - { - var modSwitch = new SelectableModSwitchSmall(mod) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Active = { Value = true } - }; - modSwitch.Active.BindValueChanged((v) => onModToggle(toggleableMod, v)); - modTogglesContainer.Add(modSwitch); - } - } } protected override void LoadComplete() @@ -85,29 +54,5 @@ namespace osu.Game.Rulesets.Osu.UI drawableRuleset.Playfield.Cursor.FadeIn(); } } - - private void onModToggle(IToggleableVisibility mod, ValueChangedEvent toggled) - { - if (toggled.NewValue) - { - mod.ToggleOnVisibility(drawableRuleset.Playfield); - } else - { - mod.ToggleOffVisibility(drawableRuleset.Playfield); - } - } - - private partial class SelectableModSwitchSmall : ModSwitchSmall - { - public SelectableModSwitchSmall(IMod mod) - : base(mod) - {} - - protected override bool OnClick(ClickEvent e) - { - Active.Value = !Active.Value; - return true; - } - } } } \ No newline at end of file diff --git a/osu.Game/Rulesets/Mods/IToggleableVisibility.cs b/osu.Game/Rulesets/Mods/IToggleableVisibility.cs deleted file mode 100644 index 5d90c24b9e..0000000000 --- a/osu.Game/Rulesets/Mods/IToggleableVisibility.cs +++ /dev/null @@ -1,11 +0,0 @@ -using osu.Game.Rulesets.UI; - -namespace osu.Game.Rulesets.Mods -{ - public interface IToggleableVisibility - { - public void ToggleOffVisibility(Playfield playfield); - - public void ToggleOnVisibility(Playfield playfield); - } -} \ No newline at end of file From f9d9df30b2104322920eb8326ad01ea946f87f06 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 22 Feb 2024 07:31:15 -0500 Subject: [PATCH 010/308] add test for HitMarkerContainer --- .../Resources/special-skin/aimmarker@2x.png | Bin 0 -> 648 bytes .../special-skin/hitmarker-left@2x.png | Bin 0 -> 2127 bytes .../special-skin/hitmarker-right@2x.png | Bin 0 -> 1742 bytes .../Resources/special-skin/skin.ini | 5 +- .../TestSceneHitMarker.cs | 134 ++++++++++++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-left@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b2a554193f08f7984568980956a3db8a6b39800 GIT binary patch literal 648 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 FV1jr`?veli literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..66be9a80ee2e2ae4c6dc6915d2b7ca5cf8ab589f GIT binary patch literal 1742 zcmV;<1~K`GP)EX>4Tx04R}tkv&MmKpe$iQ?)8p2Rn#3WT;NoK}8%(6^me@v=v%)FuC+YXws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;vxAeOiUKFoAxFnR+6_CX>@2HM@dakSAh-}000D~Nkl1L9Yb|fH_ftE7RjjO?hFdLVhHsD6;Mi3NQZON`8V+<3BQA3EPF^Lyy zBFH5ssJYD~PVPNjt#956lhOc~_xg%spW#euk$w}6}R9gK=vhuEEC9o1$c~`Rf z(Ry2wlf>QYeKL21@Rg4>x`?ys*smS!IAyn7e8E7rV7}_2!Kl|YJi?#x1%84Syp!In zkY2Z>ZC|ASeH8V&R&~)}wqQQKV8CuUoJ!|IpZh27F_`+c=GVn>f!toVMTXssZ0rPq-2F z7)4*YhF6Y6=fW|AnK={TxGQO->3NOv?ZHqu?n-9PL^x&;=hC%on8f+eXCO0UEIMoi zC$QrUyo)<T*3ZV=R?f-~Sw)q8_7}ITNXzj(Ynz8XY#0nKO|Ffd3lJ+SQw? zo^T_5u}|1KI1!G!Qa#~D8k*bxlSMfdpVSn!s$M%F!q0GjixAm?KUED#k}2O6e=Ub! zzT9@~mdieLFWo14Y(4bY?{@Z~d#M9B__8N*HojnoWl8a{S^UD*#Oe5q57df^KXo6y zFS^)9_p=4_sqNTj>tdf)v)O`qjqaxoT%0!kCVZ*Rs)f?;&EU6Nn8-Z~eiR+BtjUAq zuj+95Y2#Stj`7sR9`G*v)Lv2)QZ=X0g)O!$}Y)kHjB1^&ZygL zaa;~xW2Fp;tl;b7xLnk27M%fmLZ@PB*b@vZ9}Jg6605gAXe zstYZ)p{)uZh6ZtROM+Y(5y>UDycZfo_&zMvtXfj*ar#}oX-JZ!IZ2XB>92W{iM*rM zPniAbKjTyQRE^sNDlJm64K~qIM5Tc?-B3Fj<2yzt(TD^gFHQ4B!vebI{%dk!P-X kiyU)`Hc@tO_2Ah*0YN2jCf7K0Z2$lO07*qoM6N<$f~iM7hyVZp literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini index 9d16267d73..2952948f45 100644 --- a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini @@ -1,4 +1,7 @@ [General] Version: latest HitCircleOverlayAboveNumber: 0 -HitCirclePrefix: display \ No newline at end of file +HitCirclePrefix: display + +[Colours] +ReplayAimLine: 0,0,255 \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs new file mode 100644 index 0000000000..a0748e31af --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs @@ -0,0 +1,134 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Framework.Input.States; +using osu.Framework.Logging; +using osu.Framework.Testing.Input; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneHitMarker : OsuSkinnableTestScene + { + [Test] + public void TestHitMarkers() + { + var markerContainers = new List(); + + AddStep("Create hit markers", () => + { + markerContainers.Clear(); + SetContents(_ => { + markerContainers.Add(new TestHitMarkerContainer(new HitObjectContainer()) + { + HitMarkerEnabled = { Value = true }, + AimMarkersEnabled = { Value = true }, + AimLinesEnabled = { Value = true }, + RelativeSizeAxes = Axes.Both + }); + + return new HitMarkerInputManager + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.95f), + Child = markerContainers[^1], + }; + }); + }); + + AddUntilStep("Until skinnable expires", () => + { + if (markerContainers.Count == 0) + return false; + + Logger.Log("How many: " + markerContainers.Count); + + foreach (var markerContainer in markerContainers) + { + if (markerContainer.Children.Count != 0) + return false; + } + + return true; + }); + } + + private partial class HitMarkerInputManager : ManualInputManager + { + private double? startTime; + + public HitMarkerInputManager() + { + UseParentInput = false; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + MoveMouseTo(ToScreenSpace(DrawSize / 2)); + } + + protected override void Update() + { + base.Update(); + + const float spin_angle = 4 * MathF.PI; + + startTime ??= Time.Current; + + float fraction = (float)((Time.Current - startTime) / 5_000); + + float angle = fraction * spin_angle; + float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2; + + Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2; + MoveMouseTo(ToScreenSpace(pos)); + } + } + + private partial class TestHitMarkerContainer : HitMarkerContainer + { + private double? lastClick; + private double? startTime; + private bool finishedDrawing = false; + private bool leftOrRight = false; + + public TestHitMarkerContainer(HitObjectContainer hitObjectContainer) + : base(hitObjectContainer) + { + } + + protected override void Update() + { + base.Update(); + + if (finishedDrawing) + return; + + startTime ??= lastClick ??= Time.Current; + + if (startTime + 5_000 <= Time.Current) + { + finishedDrawing = true; + HitMarkerEnabled.Value = AimMarkersEnabled.Value = AimLinesEnabled.Value = false; + return; + } + + if (lastClick + 400 <= Time.Current) + { + OnPressed(new KeyBindingPressEvent(new InputState(), leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton)); + leftOrRight = !leftOrRight; + lastClick = Time.Current; + } + } + } + } +} \ No newline at end of file From af13389a9e8fc58ef549cb8e9920375c22dc99ef Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 22 Feb 2024 07:31:56 -0500 Subject: [PATCH 011/308] fix hit marker skinnables --- .../Default/OsuTrianglesSkinTransformer.cs | 22 ------------------- .../Legacy/OsuLegacySkinTransformer.cs | 19 ++++++++++++++++ 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs index 69ef1d9dc0..7a4c768aa2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/OsuTrianglesSkinTransformer.cs @@ -29,28 +29,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default return new DefaultJudgementPieceSliderTickMiss(result); } - break; - case OsuSkinComponentLookup osuComponent: - switch (osuComponent.Component) - { - case OsuSkinComponents.HitMarkerLeft: - if (GetTexture(@"hitmarker-left") != null) - return new HitMarker(OsuAction.LeftButton); - - return null; - - case OsuSkinComponents.HitMarkerRight: - if (GetTexture(@"hitmarker-right") != null) - return new HitMarker(OsuAction.RightButton); - - return null; - - case OsuSkinComponents.AimMarker: - if (GetTexture(@"aimmarker") != null) - return new HitMarker(); - - return null; - } break; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index a931d3ecbf..097f1732e2 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -167,6 +168,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyApproachCircle(); return null; + + case OsuSkinComponents.HitMarkerLeft: + if (GetTexture(@"hitmarker-left") != null) + return new HitMarker(OsuAction.LeftButton); + + return null; + + case OsuSkinComponents.HitMarkerRight: + if (GetTexture(@"hitmarker-right") != null) + return new HitMarker(OsuAction.RightButton); + + return null; + + case OsuSkinComponents.AimMarker: + if (GetTexture(@"aimmarker") != null) + return new HitMarker(); + + return null; } } From 45444b39d461ca9c497cc7ffdac8402b98e26e03 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 22 Feb 2024 19:01:52 -0500 Subject: [PATCH 012/308] fix formatting issues --- .../TestSceneHitMarker.cs | 11 ++++--- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Skinning/Default/DefaultHitMarker.cs | 7 ++++- .../Skinning/Default/HitMarker.cs | 7 ++++- .../Legacy/OsuLegacySkinTransformer.cs | 4 +-- .../UI/HitMarkerContainer.cs | 31 +++++++++---------- .../UI/OsuAnalysisSettings.cs | 22 ++++++------- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Screens/Play/Player.cs | 2 +- .../Play/PlayerSettings/AnalysisSettings.cs | 6 ++-- osu.Game/Screens/Play/ReplayPlayer.cs | 5 ++- 12 files changed, 53 insertions(+), 48 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs index a0748e31af..7ba681b50f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs @@ -25,8 +25,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Create hit markers", () => { markerContainers.Clear(); - SetContents(_ => { - markerContainers.Add(new TestHitMarkerContainer(new HitObjectContainer()) + SetContents(_ => + { + markerContainers.Add(new TestHitMarkerContainer(new HitObjectContainer()) { HitMarkerEnabled = { Value = true }, AimMarkersEnabled = { Value = true }, @@ -98,8 +99,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private double? lastClick; private double? startTime; - private bool finishedDrawing = false; - private bool leftOrRight = false; + private bool finishedDrawing; + private bool leftOrRight; public TestHitMarkerContainer(HitObjectContainer hitObjectContainer) : base(hitObjectContainer) @@ -131,4 +132,4 @@ namespace osu.Game.Rulesets.Osu.Tests } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index e45daed919..6dc0d5d522 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -193,4 +193,4 @@ namespace osu.Game.Rulesets.Osu.Mods approachCircle.Hide(); } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 358553ac59..224d08cbd5 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -358,6 +358,6 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } - public override AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => new OsuAnalysisSettings(drawableRuleset); + public override AnalysisSettings CreateAnalysisSettings(DrawableRuleset drawableRuleset) => new OsuAnalysisSettings(drawableRuleset); } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs index f4c11e6c8a..7dabb5182f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -61,11 +64,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default { case OsuAction.LeftButton: return (Colour4.Orange, 20, true); + case OsuAction.RightButton: return (Colour4.LightGreen, 20, true); + default: return (Colour4.Gray.Opacity(0.3F), 8, false); } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs index d86e3d4f79..28877345d0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Game.Skinning; @@ -21,13 +24,15 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default case OsuAction.LeftButton: Texture = skin.GetTexture(@"hitmarker-left"); break; + case OsuAction.RightButton: Texture = skin.GetTexture(@"hitmarker-right"); break; + default: Texture = skin.GetTexture(@"aimmarker"); break; } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 097f1732e2..61f9eebd86 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return new LegacyApproachCircle(); return null; - + case OsuSkinComponents.HitMarkerLeft: if (GetTexture(@"hitmarker-left") != null) return new HitMarker(OsuAction.LeftButton); @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy case OsuSkinComponents.AimMarker: if (GetTexture(@"aimmarker") != null) return new HitMarker(); - + return null; } } diff --git a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs index 01877a9185..e8916ea545 100644 --- a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs @@ -25,12 +25,7 @@ namespace osu.Game.Rulesets.Osu.UI { private Vector2 lastMousePosition; private Vector2? lastlastMousePosition; - private double? timePreempt; - private double TimePreempt - { - get => timePreempt ?? default_time_preempt; - set => timePreempt = value; - } + private double timePreempt; public Bindable HitMarkerEnabled = new BindableBool(); public Bindable AimMarkersEnabled = new BindableBool(); @@ -45,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.UI public HitMarkerContainer(HitObjectContainer hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; + timePreempt = default_time_preempt; } public bool OnPressed(KeyBindingPressEvent e) @@ -52,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.UI if (HitMarkerEnabled.Value && (e.Action == OsuAction.LeftButton || e.Action == OsuAction.RightButton)) { updateTimePreempt(); - AddMarker(e.Action); + addMarker(e.Action); } return false; @@ -70,33 +66,35 @@ namespace osu.Game.Rulesets.Osu.UI if (AimMarkersEnabled.Value) { updateTimePreempt(); - AddMarker(null); + addMarker(null); } if (AimLinesEnabled.Value && lastlastMousePosition != null && lastlastMousePosition != lastMousePosition) { if (!AimMarkersEnabled.Value) updateTimePreempt(); - Add(new AimLineDrawable((Vector2)lastlastMousePosition, lastMousePosition, TimePreempt)); + Add(new AimLineDrawable((Vector2)lastlastMousePosition, lastMousePosition, timePreempt)); } return base.OnMouseMove(e); } - private void AddMarker(OsuAction? action) + private void addMarker(OsuAction? action) { var component = OsuSkinComponents.AimMarker; - switch(action) + + switch (action) { case OsuAction.LeftButton: component = OsuSkinComponents.HitMarkerLeft; break; + case OsuAction.RightButton: component = OsuSkinComponents.HitMarkerRight; break; } - Add(new HitMarkerDrawable(action, component, TimePreempt) + Add(new HitMarkerDrawable(action, component, timePreempt) { Position = lastMousePosition, Origin = Anchor.Centre, @@ -110,13 +108,14 @@ namespace osu.Game.Rulesets.Osu.UI if (hitObject == null) return; - TimePreempt = hitObject.TimePreempt; + timePreempt = hitObject.TimePreempt; } private OsuHitObject? getHitObject() { foreach (var dho in hitObjectContainer.Objects) return (dho as DrawableOsuHitObject)?.HitObject; + return null; } @@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.UI LifetimeStart = Time.Current; LifetimeEnd = LifetimeStart + lifetimeDuration; - Scheduler.AddDelayed(() => + Scheduler.AddDelayed(() => { this.FadeOut(fadeOutTime); }, lifetimeDuration - fadeOutTime); @@ -186,11 +185,11 @@ namespace osu.Game.Rulesets.Osu.UI LifetimeStart = Time.Current; LifetimeEnd = LifetimeStart + lifetimeDuration; - Scheduler.AddDelayed(() => + Scheduler.AddDelayed(() => { this.FadeOut(fadeOutTime); }, lifetimeDuration - fadeOutTime); } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 319ae3e1f5..51fd835dc5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -1,14 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; using osu.Game.Localisation; -using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.PlayerSettings; @@ -16,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisSettings : AnalysisSettings { - protected new DrawableOsuRuleset drawableRuleset => (DrawableOsuRuleset)base.drawableRuleset; + protected new DrawableOsuRuleset DrawableRuleset => (DrawableOsuRuleset)base.DrawableRuleset; private readonly PlayerCheckbox hitMarkerToggle; private readonly PlayerCheckbox aimMarkerToggle; @@ -37,9 +32,9 @@ namespace osu.Game.Rulesets.Osu.UI protected override void LoadComplete() { - drawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); - drawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); - drawableRuleset.Playfield.MarkersContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); + DrawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); + DrawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); + DrawableRuleset.Playfield.MarkersContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); hideCursorToggle.Current.BindValueChanged(onCursorToggle); } @@ -48,11 +43,12 @@ namespace osu.Game.Rulesets.Osu.UI // this only hides half the cursor if (hide.NewValue) { - drawableRuleset.Playfield.Cursor.FadeOut(); - } else + DrawableRuleset.Playfield.Cursor.FadeOut(); + } + else { - drawableRuleset.Playfield.Cursor.FadeIn(); + DrawableRuleset.Playfield.Cursor.FadeIn(); } } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 5fbb23f094..84177f26b0 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -403,7 +403,7 @@ namespace osu.Game.Rulesets /// Can be overridden to alter the difficulty section to the editor beatmap setup screen. /// public virtual DifficultySection? CreateEditorDifficultySection() => null; - + public virtual AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => null; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0fa7143693..ad1f9ec897 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Play public GameplayState GameplayState { get; private set; } - protected Ruleset ruleset; + private Ruleset ruleset; public BreakOverlay BreakOverlay; diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index 122ca29142..e30d2b2d42 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -7,12 +7,12 @@ namespace osu.Game.Screens.Play.PlayerSettings { public partial class AnalysisSettings : PlayerSettingsGroup { - protected DrawableRuleset drawableRuleset; + protected DrawableRuleset DrawableRuleset; public AnalysisSettings(DrawableRuleset drawableRuleset) : base("Analysis Settings") { - this.drawableRuleset = drawableRuleset; + DrawableRuleset = drawableRuleset; } } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 0d877785e7..65e99731dc 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -72,10 +72,9 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - var analysisSettings = ruleset.CreateAnalysisSettings(DrawableRuleset); - if (analysisSettings != null) { + var analysisSettings = DrawableRuleset.Ruleset.CreateAnalysisSettings(DrawableRuleset); + if (analysisSettings != null) HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); - } } protected override void PrepareReplay() From 2a1fa8ccacd0ece5f6a098bd4951cefcd9a862c7 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Thu, 22 Feb 2024 22:34:38 -0500 Subject: [PATCH 013/308] fix OsuAnalysisSettings text not updating --- osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 51fd835dc5..0411882d2a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Localisation; @@ -30,7 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI }; } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { DrawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); DrawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); From 4d669c56a2adb102031e1ea0c63538409ace3ba8 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Fri, 23 Feb 2024 23:32:35 -0500 Subject: [PATCH 014/308] implement pooling Uses pooling for all analysis objects and creates the lifetime entries from replay data when the analysis container is constructed. --- .../UI/OsuAnalysisContainer.cs | 242 ++++++++++++++++++ .../UI/OsuAnalysisSettings.cs | 18 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 5 +- osu.Game/Rulesets/UI/AnalysisContainer.cs | 18 ++ osu.Game/Rulesets/UI/Playfield.cs | 6 + .../Play/PlayerSettings/AnalysisSettings.cs | 5 +- osu.Game/Screens/Play/ReplayPlayer.cs | 3 + 7 files changed, 284 insertions(+), 13 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs create mode 100644 osu.Game/Rulesets/UI/AnalysisContainer.cs diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs new file mode 100644 index 0000000000..a637eddd05 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -0,0 +1,242 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Performance; +using osu.Framework.Graphics.Pooling; +using osu.Game.Replays; +using osu.Game.Rulesets.Objects.Pooling; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI +{ + public partial class OsuAnalysisContainer : AnalysisContainer + { + public Bindable HitMarkerEnabled = new BindableBool(); + public Bindable AimMarkersEnabled = new BindableBool(); + public Bindable AimLinesEnabled = new BindableBool(); + + private HitMarkersContainer hitMarkersContainer; + private AimMarkersContainer aimMarkersContainer; + private AimLinesContainer aimLinesContainer; + + public OsuAnalysisContainer(Replay replay) + : base(replay) + { + InternalChildren = new Drawable[] + { + hitMarkersContainer = new HitMarkersContainer(), + aimMarkersContainer = new AimMarkersContainer() { Depth = float.MinValue }, + aimLinesContainer = new AimLinesContainer() { Depth = float.MaxValue } + }; + + HitMarkerEnabled.ValueChanged += e => hitMarkersContainer.FadeTo(e.NewValue ? 1 : 0); + AimMarkersEnabled.ValueChanged += e => aimMarkersContainer.FadeTo(e.NewValue ? 1 : 0); + AimLinesEnabled.ValueChanged += e => aimLinesContainer.FadeTo(e.NewValue ? 1 : 0); + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + var aimLineColor = skin.GetConfig(OsuSkinColour.ReplayAimLine)?.Value ?? Color4.White; + aimLineColor.A = 127; + + hitMarkersContainer.Hide(); + aimMarkersContainer.Hide(); + aimLinesContainer.Hide(); + + bool leftHeld = false; + bool rightHeld = false; + foreach (var frame in Replay.Frames) + { + var osuFrame = (OsuReplayFrame)frame; + + aimMarkersContainer.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + aimLinesContainer.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + + bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); + bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); + + if (leftHeld && !leftButton) + leftHeld = false; + else if (!leftHeld && leftButton) + { + hitMarkersContainer.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); + leftHeld = true; + } + + if (rightHeld && !rightButton) + rightHeld = false; + else if (!rightHeld && rightButton) + { + hitMarkersContainer.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); + rightHeld = true; + } + } + } + + private partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer + { + private readonly HitMarkerPool leftPool; + private readonly HitMarkerPool rightPool; + + public HitMarkersContainer() + { + AddInternal(leftPool = new HitMarkerPool(OsuSkinComponents.HitMarkerLeft, OsuAction.LeftButton, 15)); + AddInternal(rightPool = new HitMarkerPool(OsuSkinComponents.HitMarkerRight, OsuAction.RightButton, 15)); + } + + protected override HitMarkerDrawable GetDrawable(HitMarkerEntry entry) => (entry.IsLeftMarker ? leftPool : rightPool).Get(d => d.Apply(entry)); + } + + private partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer + { + private readonly HitMarkerPool pool; + + public AimMarkersContainer() + { + AddInternal(pool = new HitMarkerPool(OsuSkinComponents.AimMarker, null, 80)); + } + + protected override HitMarkerDrawable GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); + } + + private partial class AimLinesContainer : Path + { + private LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + private SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); + + public AimLinesContainer() + { + lifetimeManager.EntryBecameAlive += entryBecameAlive; + lifetimeManager.EntryBecameDead += entryBecameDead; + lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; + + PathRadius = 1f; + Colour = new Color4(255, 255, 255, 127); + } + + protected override void Update() + { + base.Update(); + + lifetimeManager.Update(Time.Current); + } + + public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); + + private void entryBecameAlive(LifetimeEntry entry) + { + aliveEntries.Add((AimPointEntry)entry); + updateVertices(); + } + + private void entryBecameDead(LifetimeEntry entry) + { + aliveEntries.Remove((AimPointEntry)entry); + updateVertices(); + } + + private void updateVertices() + { + ClearVertices(); + foreach (var entry in aliveEntries) + { + AddVertex(entry.Position); + } + } + + private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction) + { + + } + + private sealed class AimLinePointComparator : IComparer + { + public int Compare(AimPointEntry? x, AimPointEntry? y) + { + ArgumentNullException.ThrowIfNull(x); + ArgumentNullException.ThrowIfNull(y); + + return x.LifetimeStart.CompareTo(y.LifetimeStart); + } + } + } + + private partial class HitMarkerDrawable : PoolableDrawableWithLifetime + { + /// + /// This constructor only exists to meet the new() type constraint of . + /// + public HitMarkerDrawable() + { + } + + public HitMarkerDrawable(OsuSkinComponents component, OsuAction? action) + { + Origin = Anchor.Centre; + InternalChild = new SkinnableDrawable(new OsuSkinComponentLookup(component), _ => new DefaultHitMarker(action)); + } + + protected override void OnApply(AimPointEntry entry) + { + Position = entry.Position; + + using (BeginAbsoluteSequence(LifetimeStart)) + Show(); + + using (BeginAbsoluteSequence(LifetimeEnd - 200)) + this.FadeOut(200); + } + } + + private partial class HitMarkerPool : DrawablePool + { + private readonly OsuSkinComponents component; + private readonly OsuAction? action; + + public HitMarkerPool(OsuSkinComponents component, OsuAction? action, int initialSize) + : base(initialSize) + { + this.component = component; + this.action = action; + } + + protected override HitMarkerDrawable CreateNewDrawable() => new HitMarkerDrawable(component, action); + } + + private partial class AimPointEntry : LifetimeEntry + { + public Vector2 Position { get; } + + public AimPointEntry(double time, Vector2 position) + { + LifetimeStart = time; + LifetimeEnd = time + 1_000; + Position = position; + } + } + + private partial class HitMarkerEntry : AimPointEntry + { + public bool IsLeftMarker { get; } + + public HitMarkerEntry(double lifetimeStart, Vector2 position, bool isLeftMarker) + : base(lifetimeStart, position) + { + IsLeftMarker = isLeftMarker; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 0411882d2a..fd9cb67995 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Localisation; +using osu.Game.Replays; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.PlayerSettings; @@ -29,14 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI aimLinesToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimLines }, hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } }; - } - [BackgroundDependencyLoader] - private void load() - { - DrawableRuleset.Playfield.MarkersContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); - DrawableRuleset.Playfield.MarkersContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); - DrawableRuleset.Playfield.MarkersContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); hideCursorToggle.Current.BindValueChanged(onCursorToggle); } @@ -52,5 +45,14 @@ namespace osu.Game.Rulesets.Osu.UI DrawableRuleset.Playfield.Cursor.FadeIn(); } } + + public override AnalysisContainer CreateAnalysisContainer(Replay replay) + { + var analysisContainer = new OsuAnalysisContainer(replay); + analysisContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); + analysisContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); + analysisContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); + return analysisContainer; + } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 3cb3c50ef7..ea336e6067 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Osu.UI public SmokeContainer Smoke { get; } - public HitMarkerContainer MarkersContainer { get; } - public FollowPointRenderer FollowPoints { get; } public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -61,8 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, - approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, - MarkersContainer = new HitMarkerContainer(HitObjectContainer) { RelativeSizeAxes = Axes.Both } + approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both } }; HitPolicy = new StartTimeOrderedHitPolicy(); diff --git a/osu.Game/Rulesets/UI/AnalysisContainer.cs b/osu.Game/Rulesets/UI/AnalysisContainer.cs new file mode 100644 index 0000000000..62d54374e7 --- /dev/null +++ b/osu.Game/Rulesets/UI/AnalysisContainer.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Game.Replays; + +namespace osu.Game.Rulesets.UI +{ + public partial class AnalysisContainer : Container + { + protected Replay Replay; + + public AnalysisContainer(Replay replay) + { + Replay = replay; + } + } +} diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 90a2f63faa..e116acdc19 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -291,6 +291,12 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + /// + /// Adds an analysis container to internal children for replays. + /// + /// + public virtual void AddAnalysisContainer(AnalysisContainer analysisContainer) => AddInternal(analysisContainer); + #region Pooling support private readonly Dictionary pools = new Dictionary(); diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index e30d2b2d42..3752b2b900 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -1,11 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Replays; using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Play.PlayerSettings { - public partial class AnalysisSettings : PlayerSettingsGroup + public abstract partial class AnalysisSettings : PlayerSettingsGroup { protected DrawableRuleset DrawableRuleset; @@ -14,5 +15,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { DrawableRuleset = drawableRuleset; } + + public abstract AnalysisContainer CreateAnalysisContainer(Replay replay); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 65e99731dc..ce6cb5124a 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -74,7 +74,10 @@ namespace osu.Game.Screens.Play var analysisSettings = DrawableRuleset.Ruleset.CreateAnalysisSettings(DrawableRuleset); if (analysisSettings != null) + { HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); + DrawableRuleset.Playfield.AddAnalysisContainer(analysisSettings.CreateAnalysisContainer(GameplayState.Score.Replay)); + } } protected override void PrepareReplay() From c95e85368fab71e8c38e10f3dcf809f47f0986e3 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Fri, 23 Feb 2024 23:45:58 -0500 Subject: [PATCH 015/308] remove old files --- .../TestSceneHitMarker.cs | 135 ------------ .../UI/HitMarkerContainer.cs | 195 ------------------ 2 files changed, 330 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs delete mode 100644 osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs deleted file mode 100644 index 7ba681b50f..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Framework.Input.Events; -using osu.Framework.Input.States; -using osu.Framework.Logging; -using osu.Framework.Testing.Input; -using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.UI; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Tests -{ - public partial class TestSceneHitMarker : OsuSkinnableTestScene - { - [Test] - public void TestHitMarkers() - { - var markerContainers = new List(); - - AddStep("Create hit markers", () => - { - markerContainers.Clear(); - SetContents(_ => - { - markerContainers.Add(new TestHitMarkerContainer(new HitObjectContainer()) - { - HitMarkerEnabled = { Value = true }, - AimMarkersEnabled = { Value = true }, - AimLinesEnabled = { Value = true }, - RelativeSizeAxes = Axes.Both - }); - - return new HitMarkerInputManager - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.95f), - Child = markerContainers[^1], - }; - }); - }); - - AddUntilStep("Until skinnable expires", () => - { - if (markerContainers.Count == 0) - return false; - - Logger.Log("How many: " + markerContainers.Count); - - foreach (var markerContainer in markerContainers) - { - if (markerContainer.Children.Count != 0) - return false; - } - - return true; - }); - } - - private partial class HitMarkerInputManager : ManualInputManager - { - private double? startTime; - - public HitMarkerInputManager() - { - UseParentInput = false; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - MoveMouseTo(ToScreenSpace(DrawSize / 2)); - } - - protected override void Update() - { - base.Update(); - - const float spin_angle = 4 * MathF.PI; - - startTime ??= Time.Current; - - float fraction = (float)((Time.Current - startTime) / 5_000); - - float angle = fraction * spin_angle; - float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2; - - Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2; - MoveMouseTo(ToScreenSpace(pos)); - } - } - - private partial class TestHitMarkerContainer : HitMarkerContainer - { - private double? lastClick; - private double? startTime; - private bool finishedDrawing; - private bool leftOrRight; - - public TestHitMarkerContainer(HitObjectContainer hitObjectContainer) - : base(hitObjectContainer) - { - } - - protected override void Update() - { - base.Update(); - - if (finishedDrawing) - return; - - startTime ??= lastClick ??= Time.Current; - - if (startTime + 5_000 <= Time.Current) - { - finishedDrawing = true; - HitMarkerEnabled.Value = AimMarkersEnabled.Value = AimLinesEnabled.Value = false; - return; - } - - if (lastClick + 400 <= Time.Current) - { - OnPressed(new KeyBindingPressEvent(new InputState(), leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton)); - leftOrRight = !leftOrRight; - lastClick = Time.Current; - } - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs deleted file mode 100644 index e8916ea545..0000000000 --- a/osu.Game.Rulesets.Osu/UI/HitMarkerContainer.cs +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Skinning; -using osu.Game.Rulesets.Osu.Skinning.Default; -using osu.Game.Rulesets.UI; -using osu.Game.Skinning; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Osu.UI -{ - public partial class HitMarkerContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler - { - private Vector2 lastMousePosition; - private Vector2? lastlastMousePosition; - private double timePreempt; - - public Bindable HitMarkerEnabled = new BindableBool(); - public Bindable AimMarkersEnabled = new BindableBool(); - public Bindable AimLinesEnabled = new BindableBool(); - - private const double default_time_preempt = 1000; - - private readonly HitObjectContainer hitObjectContainer; - - public override bool ReceivePositionalInputAt(Vector2 _) => true; - - public HitMarkerContainer(HitObjectContainer hitObjectContainer) - { - this.hitObjectContainer = hitObjectContainer; - timePreempt = default_time_preempt; - } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (HitMarkerEnabled.Value && (e.Action == OsuAction.LeftButton || e.Action == OsuAction.RightButton)) - { - updateTimePreempt(); - addMarker(e.Action); - } - - return false; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - lastlastMousePosition = lastMousePosition; - lastMousePosition = e.MousePosition; - - if (AimMarkersEnabled.Value) - { - updateTimePreempt(); - addMarker(null); - } - - if (AimLinesEnabled.Value && lastlastMousePosition != null && lastlastMousePosition != lastMousePosition) - { - if (!AimMarkersEnabled.Value) - updateTimePreempt(); - Add(new AimLineDrawable((Vector2)lastlastMousePosition, lastMousePosition, timePreempt)); - } - - return base.OnMouseMove(e); - } - - private void addMarker(OsuAction? action) - { - var component = OsuSkinComponents.AimMarker; - - switch (action) - { - case OsuAction.LeftButton: - component = OsuSkinComponents.HitMarkerLeft; - break; - - case OsuAction.RightButton: - component = OsuSkinComponents.HitMarkerRight; - break; - } - - Add(new HitMarkerDrawable(action, component, timePreempt) - { - Position = lastMousePosition, - Origin = Anchor.Centre, - Depth = action == null ? float.MaxValue : float.MinValue - }); - } - - private void updateTimePreempt() - { - var hitObject = getHitObject(); - if (hitObject == null) - return; - - timePreempt = hitObject.TimePreempt; - } - - private OsuHitObject? getHitObject() - { - foreach (var dho in hitObjectContainer.Objects) - return (dho as DrawableOsuHitObject)?.HitObject; - - return null; - } - - private partial class HitMarkerDrawable : SkinnableDrawable - { - private readonly double lifetimeDuration; - private readonly double fadeOutTime; - - public override bool RemoveWhenNotAlive => true; - - public HitMarkerDrawable(OsuAction? action, OsuSkinComponents componenet, double timePreempt) - : base(new OsuSkinComponentLookup(componenet), _ => new DefaultHitMarker(action)) - { - fadeOutTime = timePreempt / 2; - lifetimeDuration = timePreempt + fadeOutTime; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - LifetimeStart = Time.Current; - LifetimeEnd = LifetimeStart + lifetimeDuration; - - Scheduler.AddDelayed(() => - { - this.FadeOut(fadeOutTime); - }, lifetimeDuration - fadeOutTime); - } - } - - private partial class AimLineDrawable : CompositeDrawable - { - private readonly double lifetimeDuration; - private readonly double fadeOutTime; - - public override bool RemoveWhenNotAlive => true; - - public AimLineDrawable(Vector2 fromP, Vector2 toP, double timePreempt) - { - fadeOutTime = timePreempt / 2; - lifetimeDuration = timePreempt + fadeOutTime; - - float distance = Vector2.Distance(fromP, toP); - Vector2 direction = (toP - fromP); - InternalChild = new Box - { - Position = fromP + (direction / 2), - Size = new Vector2(distance, 1), - Rotation = (float)(Math.Atan(direction.Y / direction.X) * (180 / Math.PI)), - Origin = Anchor.Centre - }; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - var color = skin.GetConfig(OsuSkinColour.ReplayAimLine)?.Value ?? Color4.White; - color.A = 127; - InternalChild.Colour = color; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - LifetimeStart = Time.Current; - LifetimeEnd = LifetimeStart + lifetimeDuration; - - Scheduler.AddDelayed(() => - { - this.FadeOut(fadeOutTime); - }, lifetimeDuration - fadeOutTime); - } - } - } -} From 8cdd9c9ddcec526dcf4866f2c0b0ce1b98dbffd4 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Fri, 23 Feb 2024 23:46:50 -0500 Subject: [PATCH 016/308] skinning changes improve default design --- .../Skinning/Default/DefaultHitMarker.cs | 8 +++----- osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs | 1 - osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs | 6 +----- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs index 7dabb5182f..dc890d4d63 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs @@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(3, length), - Rotation = 45, Colour = Colour4.Black.Opacity(0.5F) }, new Box @@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(3, length), - Rotation = 135, + Rotation = 90, Colour = Colour4.Black.Opacity(0.5F) } }; @@ -44,7 +43,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(1, length), - Rotation = 45, Colour = colour }, new Box @@ -52,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(1, length), - Rotation = 135, + Rotation = 90, Colour = colour } }); @@ -69,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default return (Colour4.LightGreen, 20, true); default: - return (Colour4.Gray.Opacity(0.3F), 8, false); + return (Colour4.Gray.Opacity(0.5F), 8, false); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs index 5c864fb6c2..24f9217a5f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs @@ -10,6 +10,5 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderBall, SpinnerBackground, StarBreakAdditive, - ReplayAimLine, } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index a637eddd05..68686f835d 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Pooling; using osu.Game.Replays; using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.UI; using osu.Game.Skinning; @@ -47,11 +46,8 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load() { - var aimLineColor = skin.GetConfig(OsuSkinColour.ReplayAimLine)?.Value ?? Color4.White; - aimLineColor.A = 127; - hitMarkersContainer.Hide(); aimMarkersContainer.Hide(); aimLinesContainer.Hide(); From 822ecb71068b77e071fb590983ff2834b0f9b2e3 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Fri, 23 Feb 2024 23:52:17 -0500 Subject: [PATCH 017/308] remove unnecessary changes --- osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini | 5 +---- osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs | 6 ------ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini index 2952948f45..9d16267d73 100644 --- a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini +++ b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini @@ -1,7 +1,4 @@ [General] Version: latest HitCircleOverlayAboveNumber: 0 -HitCirclePrefix: display - -[Colours] -ReplayAimLine: 0,0,255 \ No newline at end of file +HitCirclePrefix: display \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 68686f835d..c4b5135ca2 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -117,7 +117,6 @@ namespace osu.Game.Rulesets.Osu.UI { lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; - lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary; PathRadius = 1f; Colour = new Color4(255, 255, 255, 127); @@ -153,11 +152,6 @@ namespace osu.Game.Rulesets.Osu.UI } } - private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction) - { - - } - private sealed class AimLinePointComparator : IComparer { public int Compare(AimPointEntry? x, AimPointEntry? y) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index ea336e6067..a801e3d2f7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, HitObjectContainer, judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, - approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both } + approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; HitPolicy = new StartTimeOrderedHitPolicy(); From 29e5f409bac928bc49e1940a2e44b8ee4b8f2a70 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Tue, 27 Feb 2024 21:51:54 -0500 Subject: [PATCH 018/308] remove skinnable better to add skinnable elements later --- .../Resources/special-skin/aimmarker@2x.png | Bin 648 -> 0 bytes .../special-skin/hitmarker-left@2x.png | Bin 2127 -> 0 bytes .../special-skin/hitmarker-right@2x.png | Bin 1742 -> 0 bytes osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 3 - .../Skinning/Default/DefaultHitMarker.cs | 74 ------------------ .../Skinning/Default/HitMarker.cs | 68 ++++++++++++---- .../Legacy/OsuLegacySkinTransformer.cs | 20 +---- .../UI/OsuAnalysisContainer.cs | 69 ++++++++-------- 8 files changed, 88 insertions(+), 146 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-left@2x.png delete mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png delete mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/aimmarker@2x.png deleted file mode 100644 index 0b2a554193f08f7984568980956a3db8a6b39800..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 648 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 FV1jr`?veli diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitmarker-right@2x.png deleted file mode 100644 index 66be9a80ee2e2ae4c6dc6915d2b7ca5cf8ab589f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1742 zcmV;<1~K`GP)EX>4Tx04R}tkv&MmKpe$iQ?)8p2Rn#3WT;NoK}8%(6^me@v=v%)FuC+YXws0R zxHt-~1qVMCs}3&Cx;nTDg5U>;vxAeOiUKFoAxFnR+6_CX>@2HM@dakSAh-}000D~Nkl1L9Yb|fH_ftE7RjjO?hFdLVhHsD6;Mi3NQZON`8V+<3BQA3EPF^Lyy zBFH5ssJYD~PVPNjt#956lhOc~_xg%spW#euk$w}6}R9gK=vhuEEC9o1$c~`Rf z(Ry2wlf>QYeKL21@Rg4>x`?ys*smS!IAyn7e8E7rV7}_2!Kl|YJi?#x1%84Syp!In zkY2Z>ZC|ASeH8V&R&~)}wqQQKV8CuUoJ!|IpZh27F_`+c=GVn>f!toVMTXssZ0rPq-2F z7)4*YhF6Y6=fW|AnK={TxGQO->3NOv?ZHqu?n-9PL^x&;=hC%on8f+eXCO0UEIMoi zC$QrUyo)<T*3ZV=R?f-~Sw)q8_7}ITNXzj(Ynz8XY#0nKO|Ffd3lJ+SQw? zo^T_5u}|1KI1!G!Qa#~D8k*bxlSMfdpVSn!s$M%F!q0GjixAm?KUED#k}2O6e=Ub! zzT9@~mdieLFWo14Y(4bY?{@Z~d#M9B__8N*HojnoWl8a{S^UD*#Oe5q57df^KXo6y zFS^)9_p=4_sqNTj>tdf)v)O`qjqaxoT%0!kCVZ*Rs)f?;&EU6Nn8-Z~eiR+BtjUAq zuj+95Y2#Stj`7sR9`G*v)Lv2)QZ=X0g)O!$}Y)kHjB1^&ZygL zaa;~xW2Fp;tl;b7xLnk27M%fmLZ@PB*b@vZ9}Jg6605gAXe zstYZ)p{)uZh6ZtROM+Y(5y>UDycZfo_&zMvtXfj*ar#}oX-JZ!IZ2XB>92W{iM*rM zPniAbKjTyQRE^sNDlJm64K~qIM5Tc?-B3Fj<2yzt(TD^gFHQ4B!vebI{%dk!P-X kiyU)`Hc@tO_2Ah*0YN2jCf7K0Z2$lO07*qoM6N<$f~iM7hyVZp diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 75db18d345..52fdfea95f 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -22,8 +22,5 @@ namespace osu.Game.Rulesets.Osu SpinnerBody, CursorSmoke, ApproachCircle, - HitMarkerLeft, - HitMarkerRight, - AimMarker } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs deleted file mode 100644 index dc890d4d63..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultHitMarker.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Skinning.Default -{ - public partial class DefaultHitMarker : CompositeDrawable - { - public DefaultHitMarker(OsuAction? action) - { - var (colour, length, hasBorder) = getConfig(action); - - if (hasBorder) - { - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) - } - }; - } - - AddRangeInternal(new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Colour = colour - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - Colour = colour - } - }); - } - - private (Colour4 colour, float length, bool hasBorder) getConfig(OsuAction? action) - { - switch (action) - { - case OsuAction.LeftButton: - return (Colour4.Orange, 20, true); - - case OsuAction.RightButton: - return (Colour4.LightGreen, 20, true); - - default: - return (Colour4.Gray.Opacity(0.5F), 8, false); - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs index 28877345d0..fe662470bc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs @@ -1,37 +1,73 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; -using osu.Game.Skinning; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Default { - public partial class HitMarker : Sprite + public partial class HitMarker : CompositeDrawable { - private readonly OsuAction? action; - - public HitMarker(OsuAction? action = null) + public HitMarker(OsuAction? action) { - this.action = action; + var (colour, length, hasBorder) = getConfig(action); + + if (hasBorder) + { + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 90, + Colour = Colour4.Black.Opacity(0.5F) + } + }; + } + + AddRangeInternal(new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Colour = colour + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 90, + Colour = colour + } + }); } - [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private (Colour4 colour, float length, bool hasBorder) getConfig(OsuAction? action) { switch (action) { case OsuAction.LeftButton: - Texture = skin.GetTexture(@"hitmarker-left"); - break; + return (Colour4.Orange, 20, true); case OsuAction.RightButton: - Texture = skin.GetTexture(@"hitmarker-right"); - break; + return (Colour4.LightGreen, 20, true); default: - Texture = skin.GetTexture(@"aimmarker"); - break; + return (Colour4.Gray.Opacity(0.5F), 8, false); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 61f9eebd86..d2ebc68c52 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; -using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; namespace osu.Game.Rulesets.Osu.Skinning.Legacy @@ -169,23 +168,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; - case OsuSkinComponents.HitMarkerLeft: - if (GetTexture(@"hitmarker-left") != null) - return new HitMarker(OsuAction.LeftButton); - - return null; - - case OsuSkinComponents.HitMarkerRight: - if (GetTexture(@"hitmarker-right") != null) - return new HitMarker(OsuAction.RightButton); - - return null; - - case OsuSkinComponents.AimMarker: - if (GetTexture(@"aimmarker") != null) - return new HitMarker(); - - return null; + default: + throw new UnsupportedSkinComponentException(lookup); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index c4b5135ca2..7d8ae6980c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.UI; -using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -26,40 +25,41 @@ namespace osu.Game.Rulesets.Osu.UI public Bindable AimMarkersEnabled = new BindableBool(); public Bindable AimLinesEnabled = new BindableBool(); - private HitMarkersContainer hitMarkersContainer; - private AimMarkersContainer aimMarkersContainer; - private AimLinesContainer aimLinesContainer; + protected HitMarkersContainer HitMarkers; + protected AimMarkersContainer AimMarkers; + protected AimLinesContainer AimLines; public OsuAnalysisContainer(Replay replay) : base(replay) { InternalChildren = new Drawable[] { - hitMarkersContainer = new HitMarkersContainer(), - aimMarkersContainer = new AimMarkersContainer() { Depth = float.MinValue }, - aimLinesContainer = new AimLinesContainer() { Depth = float.MaxValue } + HitMarkers = new HitMarkersContainer(), + AimMarkers = new AimMarkersContainer { Depth = float.MinValue }, + AimLines = new AimLinesContainer { Depth = float.MaxValue } }; - HitMarkerEnabled.ValueChanged += e => hitMarkersContainer.FadeTo(e.NewValue ? 1 : 0); - AimMarkersEnabled.ValueChanged += e => aimMarkersContainer.FadeTo(e.NewValue ? 1 : 0); - AimLinesEnabled.ValueChanged += e => aimLinesContainer.FadeTo(e.NewValue ? 1 : 0); + HitMarkerEnabled.ValueChanged += e => HitMarkers.FadeTo(e.NewValue ? 1 : 0); + AimMarkersEnabled.ValueChanged += e => AimMarkers.FadeTo(e.NewValue ? 1 : 0); + AimLinesEnabled.ValueChanged += e => AimLines.FadeTo(e.NewValue ? 1 : 0); } [BackgroundDependencyLoader] private void load() { - hitMarkersContainer.Hide(); - aimMarkersContainer.Hide(); - aimLinesContainer.Hide(); + HitMarkers.Hide(); + AimMarkers.Hide(); + AimLines.Hide(); bool leftHeld = false; bool rightHeld = false; + foreach (var frame in Replay.Frames) { var osuFrame = (OsuReplayFrame)frame; - aimMarkersContainer.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); - aimLinesContainer.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + AimMarkers.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + AimLines.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI leftHeld = false; else if (!leftHeld && leftButton) { - hitMarkersContainer.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); + HitMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); leftHeld = true; } @@ -76,42 +76,42 @@ namespace osu.Game.Rulesets.Osu.UI rightHeld = false; else if (!rightHeld && rightButton) { - hitMarkersContainer.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); + HitMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); rightHeld = true; } } } - private partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer + protected partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer { private readonly HitMarkerPool leftPool; private readonly HitMarkerPool rightPool; public HitMarkersContainer() { - AddInternal(leftPool = new HitMarkerPool(OsuSkinComponents.HitMarkerLeft, OsuAction.LeftButton, 15)); - AddInternal(rightPool = new HitMarkerPool(OsuSkinComponents.HitMarkerRight, OsuAction.RightButton, 15)); + AddInternal(leftPool = new HitMarkerPool(OsuAction.LeftButton, 15)); + AddInternal(rightPool = new HitMarkerPool(OsuAction.RightButton, 15)); } protected override HitMarkerDrawable GetDrawable(HitMarkerEntry entry) => (entry.IsLeftMarker ? leftPool : rightPool).Get(d => d.Apply(entry)); } - private partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer + protected partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer { private readonly HitMarkerPool pool; public AimMarkersContainer() { - AddInternal(pool = new HitMarkerPool(OsuSkinComponents.AimMarker, null, 80)); + AddInternal(pool = new HitMarkerPool(null, 80)); } protected override HitMarkerDrawable GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); } - private partial class AimLinesContainer : Path + protected partial class AimLinesContainer : Path { - private LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - private SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); public AimLinesContainer() { @@ -146,6 +146,7 @@ namespace osu.Game.Rulesets.Osu.UI private void updateVertices() { ClearVertices(); + foreach (var entry in aliveEntries) { AddVertex(entry.Position); @@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.UI } } - private partial class HitMarkerDrawable : PoolableDrawableWithLifetime + protected partial class HitMarkerDrawable : PoolableDrawableWithLifetime { /// /// This constructor only exists to meet the new() type constraint of . @@ -173,10 +174,10 @@ namespace osu.Game.Rulesets.Osu.UI { } - public HitMarkerDrawable(OsuSkinComponents component, OsuAction? action) + public HitMarkerDrawable(OsuAction? action) { Origin = Anchor.Centre; - InternalChild = new SkinnableDrawable(new OsuSkinComponentLookup(component), _ => new DefaultHitMarker(action)); + InternalChild = new HitMarker(action); } protected override void OnApply(AimPointEntry entry) @@ -191,22 +192,20 @@ namespace osu.Game.Rulesets.Osu.UI } } - private partial class HitMarkerPool : DrawablePool + protected partial class HitMarkerPool : DrawablePool { - private readonly OsuSkinComponents component; private readonly OsuAction? action; - public HitMarkerPool(OsuSkinComponents component, OsuAction? action, int initialSize) + public HitMarkerPool(OsuAction? action, int initialSize) : base(initialSize) { - this.component = component; this.action = action; } - protected override HitMarkerDrawable CreateNewDrawable() => new HitMarkerDrawable(component, action); + protected override HitMarkerDrawable CreateNewDrawable() => new HitMarkerDrawable(action); } - private partial class AimPointEntry : LifetimeEntry + protected partial class AimPointEntry : LifetimeEntry { public Vector2 Position { get; } @@ -218,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.UI } } - private partial class HitMarkerEntry : AimPointEntry + protected partial class HitMarkerEntry : AimPointEntry { public bool IsLeftMarker { get; } From cefc8357bbdb732f805e848b065afcf40619b9c0 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Tue, 27 Feb 2024 21:52:10 -0500 Subject: [PATCH 019/308] test scene for OsuAnalysisContainer --- .../TestSceneHitMarker.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs new file mode 100644 index 0000000000..e4c48f96b8 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneHitMarker : OsuTestScene + { + private TestOsuAnalysisContainer analysisContainer; + + [Test] + public void TestHitMarkers() + { + createAnalysisContainer(); + AddStep("enable hit markers", () => analysisContainer.HitMarkerEnabled.Value = true); + AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); + AddStep("disable hit markers", () => analysisContainer.HitMarkerEnabled.Value = false); + AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); + } + + [Test] + public void TestAimMarker() + { + createAnalysisContainer(); + AddStep("enable aim markers", () => analysisContainer.AimMarkersEnabled.Value = true); + AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); + AddStep("disable aim markers", () => analysisContainer.AimMarkersEnabled.Value = false); + AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); + } + + [Test] + public void TestAimLines() + { + createAnalysisContainer(); + AddStep("enable aim lines", () => analysisContainer.AimLinesEnabled.Value = true); + AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); + AddStep("disable aim lines", () => analysisContainer.AimLinesEnabled.Value = false); + AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); + } + + private void createAnalysisContainer() + { + AddStep("create new analysis container", () => Child = analysisContainer = new TestOsuAnalysisContainer(fabricateReplay())); + } + + private Replay fabricateReplay() + { + var frames = new List(); + + for (int i = 0; i < 50; i++) + { + frames.Add(new OsuReplayFrame + { + Time = Time.Current + i * 15, + Position = new Vector2(20 + i * 10, 20), + Actions = + { + i % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton + } + }); + } + + return new Replay { Frames = frames }; + } + + private partial class TestOsuAnalysisContainer : OsuAnalysisContainer + { + public TestOsuAnalysisContainer(Replay replay) + : base(replay) + { + } + + public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); + + public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); + + public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; + } + } +} From 7687ab63edd2b49b4e1a5a2c30be2b9e2b8ca328 Mon Sep 17 00:00:00 2001 From: Sheepposu Date: Tue, 27 Feb 2024 22:03:45 -0500 Subject: [PATCH 020/308] fix code formatting --- osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs | 3 ++- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 1 - osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index fd9cb67995..b3d5231ade 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -16,12 +16,13 @@ namespace osu.Game.Rulesets.Osu.UI private readonly PlayerCheckbox hitMarkerToggle; private readonly PlayerCheckbox aimMarkerToggle; - private readonly PlayerCheckbox hideCursorToggle; private readonly PlayerCheckbox aimLinesToggle; public OsuAnalysisSettings(DrawableRuleset drawableRuleset) : base(drawableRuleset) { + PlayerCheckbox hideCursorToggle; + Children = new Drawable[] { hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index a801e3d2f7..411a02c5af 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -36,7 +36,6 @@ namespace osu.Game.Rulesets.Osu.UI private readonly JudgementPooler judgementPooler; public SmokeContainer Smoke { get; } - public FollowPointRenderer FollowPoints { get; } public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index 3752b2b900..91531b28b4 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { protected DrawableRuleset DrawableRuleset; - public AnalysisSettings(DrawableRuleset drawableRuleset) + protected AnalysisSettings(DrawableRuleset drawableRuleset) : base("Analysis Settings") { DrawableRuleset = drawableRuleset; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index ce6cb5124a..81cc44d889 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -73,6 +73,7 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); var analysisSettings = DrawableRuleset.Ruleset.CreateAnalysisSettings(DrawableRuleset); + if (analysisSettings != null) { HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); From 0ee89183cc28b3cd39e14c30a4ec83a353a9db9d Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Wed, 26 Jun 2024 15:25:41 -0400 Subject: [PATCH 021/308] initial implementation --- osu.Desktop/OsuGameDesktop.cs | 34 +++----- osu.Desktop/Program.cs | 39 +++------ ...lUpdateManager.cs => VeloUpdateManager.cs} | 86 ++++--------------- osu.Desktop/osu.Desktop.csproj | 2 +- 4 files changed, 38 insertions(+), 123 deletions(-) rename osu.Desktop/Updater/{SquirrelUpdateManager.cs => VeloUpdateManager.cs} (52%) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index e8783c997a..245d00dc53 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -19,7 +19,6 @@ using osu.Desktop.Windows; using osu.Framework.Allocation; using osu.Game.IO; using osu.Game.IPC; -using osu.Game.Online.Multiplayer; using osu.Game.Performance; using osu.Game.Utils; using SDL; @@ -103,35 +102,22 @@ namespace osu.Desktop if (!string.IsNullOrEmpty(packageManaged)) return new NoActionUpdateManager(); - switch (RuntimeInfo.OS) - { - case RuntimeInfo.Platform.Windows: - Debug.Assert(OperatingSystem.IsWindows()); - - return new SquirrelUpdateManager(); - - default: - return new SimpleUpdateManager(); - } + return new VeloUpdateManager(); } public override bool RestartAppWhenExited() { - switch (RuntimeInfo.OS) + try { - case RuntimeInfo.Platform.Windows: - Debug.Assert(OperatingSystem.IsWindows()); - - // Of note, this is an async method in squirrel that adds an arbitrary delay before returning - // likely to ensure the external process is in a good state. - // - // We're not waiting on that here, but the outro playing before the actual exit should be enough - // to cover this. - Squirrel.UpdateManager.RestartAppWhenExited().FireAndForget(); - return true; + Process.Start(Process.GetCurrentProcess().MainModule?.FileName ?? throw new InvalidOperationException()); + Environment.Exit(0); + return true; + } + catch (Exception e) + { + Logger.Error(e, "Failed to restart application"); + return base.RestartAppWhenExited(); } - - return base.RestartAppWhenExited(); } protected override void LoadComplete() diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 23e56cdce9..7c23c15d5a 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.Runtime.Versioning; using osu.Desktop.LegacyIpc; using osu.Desktop.Windows; using osu.Framework; @@ -14,7 +13,7 @@ using osu.Game; using osu.Game.IPC; using osu.Game.Tournament; using SDL; -using Squirrel; +using Velopack; namespace osu.Desktop { @@ -66,10 +65,10 @@ namespace osu.Desktop return; } } - - setupSquirrel(); } + setupVelo(); + // NVIDIA profiles are based on the executable name of a process. // Lazer and stable share the same executable name. // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. @@ -177,32 +176,14 @@ namespace osu.Desktop return false; } - [SupportedOSPlatform("windows")] - private static void setupSquirrel() + private static void setupVelo() { - SquirrelAwareApp.HandleEvents(onInitialInstall: (_, tools) => - { - tools.CreateShortcutForThisExe(); - tools.CreateUninstallerRegistryEntry(); - WindowsAssociationManager.InstallAssociations(); - }, onAppUpdate: (_, tools) => - { - tools.CreateUninstallerRegistryEntry(); - WindowsAssociationManager.UpdateAssociations(); - }, onAppUninstall: (_, tools) => - { - tools.RemoveShortcutForThisExe(); - tools.RemoveUninstallerRegistryEntry(); - WindowsAssociationManager.UninstallAssociations(); - }, onEveryRun: (_, _, _) => - { - // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently - // causes the right-click context menu to function incorrectly. - // - // This may turn out to be non-required after an alternative solution is implemented. - // see https://github.com/clowd/Clowd.Squirrel/issues/24 - // tools.SetProcessAppUserModelId(); - }); + VelopackApp + .Build() + .WithFirstRun(v => + { + if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations(); + }).Run(); } } } diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/VeloUpdateManager.cs similarity index 52% rename from osu.Desktop/Updater/SquirrelUpdateManager.cs rename to osu.Desktop/Updater/VeloUpdateManager.cs index dba157a6e9..8fc68f77cd 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/VeloUpdateManager.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Runtime.Versioning; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Logging; @@ -10,30 +9,15 @@ using osu.Game; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; -using Squirrel.SimpleSplat; -using Squirrel.Sources; -using LogLevel = Squirrel.SimpleSplat.LogLevel; -using UpdateManager = osu.Game.Updater.UpdateManager; +using osu.Game.Updater; namespace osu.Desktop.Updater { - [SupportedOSPlatform("windows")] - public partial class SquirrelUpdateManager : UpdateManager + public partial class VeloUpdateManager : UpdateManager { - private Squirrel.UpdateManager? updateManager; + private Velopack.UpdateManager? updateManager; private INotificationOverlay notificationOverlay = null!; - public Task PrepareUpdateAsync() => Squirrel.UpdateManager.RestartAppWhenExited(); - - private static readonly Logger logger = Logger.GetLogger("updater"); - - /// - /// Whether an update has been downloaded but not yet applied. - /// - private bool updatePending; - - private readonly SquirrelLogger squirrelLogger = new SquirrelLogger(); - [Resolved] private OsuGameBase game { get; set; } = null!; @@ -44,13 +28,11 @@ namespace osu.Desktop.Updater private void load(INotificationOverlay notifications) { notificationOverlay = notifications; - - SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger)); } protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); - private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification? notification = null) + private async Task checkForUpdateAsync(UpdateProgressNotification? notification = null) { // should we schedule a retry on completion of this check? bool scheduleRecheck = true; @@ -63,27 +45,27 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; - updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer"); + updateManager ??= new Velopack.UpdateManager(new Velopack.Sources.GithubSource(@"https://github.com/ppy/osu", github_token, false)); - var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); + var info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); - if (info.ReleasesToApply.Count == 0) + if (info == null) { - if (updatePending) + // If there is an update pending restart, show the notification again. + if (updateManager.IsUpdatePendingRestart) { - // the user may have dismissed the completion notice, so show it again. notificationOverlay.Post(new UpdateApplicationCompleteNotification { Activated = () => { restartToApplyUpdate(); return true; - }, + } }); return true; } - // no updates available. bail and retry later. + // Otherwise there's no updates available. Bail and retry later. return false; } @@ -103,31 +85,17 @@ namespace osu.Desktop.Updater try { - await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false); + await updateManager.DownloadUpdatesAsync(info, p => notification.Progress = p / 100f).ConfigureAwait(false); notification.StartInstall(); - await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false); - notification.State = ProgressNotificationState.Completed; - updatePending = true; } catch (Exception e) { - if (useDeltaPatching) - { - logger.Add(@"delta patching failed; will attempt full download!"); - - // could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959) - // try again without deltas. - await checkForUpdateAsync(false, notification).ConfigureAwait(false); - } - else - { // In the case of an error, a separate notification will be displayed. notification.FailDownload(); Logger.Error(e, @"update failed!"); - } } } catch (Exception) @@ -149,32 +117,12 @@ namespace osu.Desktop.Updater private bool restartToApplyUpdate() { - PrepareUpdateAsync() - .ContinueWith(_ => Schedule(() => game.AttemptExit())); + if (updateManager == null) + return false; + + updateManager.WaitExitThenApplyUpdates(null); + Schedule(() => game.AttemptExit()); return true; } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - updateManager?.Dispose(); - } - - private class SquirrelLogger : ILogger, IDisposable - { - public LogLevel Level { get; set; } = LogLevel.Info; - - public void Write(string message, LogLevel logLevel) - { - if (logLevel < Level) - return; - - logger.Add(message); - } - - public void Dispose() - { - } - } } } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index e7a63bd921..7df82e1281 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -23,10 +23,10 @@ - + From 1025e5b3cc756b1cbd01216be157b3e0b270225f Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Wed, 26 Jun 2024 21:55:22 -0400 Subject: [PATCH 022/308] rewrite the restart function --- osu.Desktop/OsuGameDesktop.cs | 11 ++++++++--- .../Screens/Setup/TournamentSwitcher.cs | 3 +-- osu.Game/OsuGameBase.cs | 2 +- .../Settings/Sections/Graphics/RendererSettings.cs | 3 +-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 245d00dc53..3019aefdc0 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -105,18 +105,23 @@ namespace osu.Desktop return new VeloUpdateManager(); } - public override bool RestartAppWhenExited() + public override bool RestartApp() { try { - Process.Start(Process.GetCurrentProcess().MainModule?.FileName ?? throw new InvalidOperationException()); + var startInfo = new ProcessStartInfo + { + FileName = Process.GetCurrentProcess().MainModule!.FileName, + UseShellExecute = true + }; + Process.Start(startInfo); Environment.Exit(0); return true; } catch (Exception e) { Logger.Error(e, "Failed to restart application"); - return base.RestartAppWhenExited(); + return base.RestartApp(); } } diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index e55cbc2dbb..69ead451ba 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -31,8 +31,7 @@ namespace osu.Game.Tournament.Screens.Setup Action = () => { - game.RestartAppWhenExited(); - game.AttemptExit(); + game.RestartApp(); }; folderButton.Action = () => storage.PresentExternally(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5533ee8337..db603f0046 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -513,7 +513,7 @@ namespace osu.Game /// If supported by the platform, the game will automatically restart after the next exit. /// /// Whether a restart operation was queued. - public virtual bool RestartAppWhenExited() => false; + public virtual bool RestartApp() => false; public bool Migrate(string path) { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index a8b127d522..17e345c2c8 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -67,9 +67,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (r.NewValue == RendererType.Automatic && automaticRendererInUse) return; - if (game?.RestartAppWhenExited() == true) + if (game?.RestartApp() == true) { - game.AttemptExit(); } else { From 36a3765ee4e2ef3240a265549ebc6a734f696c62 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 27 Jun 2024 12:57:24 -0400 Subject: [PATCH 023/308] Replace with attemptexit to better display how restarting is borked --- osu.Desktop/OsuGameDesktop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index b6053edf77..28f3d3dc5d 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -114,7 +114,7 @@ namespace osu.Desktop UseShellExecute = true }; Process.Start(startInfo); - Environment.Exit(0); + base.AttemptExit(); return true; } catch (Exception e) From b15028a918ececff597d694c3315d732cf784cdc Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 12:36:12 +0200 Subject: [PATCH 024/308] fixes --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 8d80ed651e..7720afe60a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; @@ -23,18 +24,9 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSelectionHandler : EditorSelectionHandler { - [Resolved(CanBeNull = true)] - private IDistanceSnapProvider? snapProvider { get; set; } - [Resolved] private OsuGridToolboxGroup gridToolbox { get; set; } = null!; - /// - /// During a transform, the initial path types of a single selected slider are stored so they - /// can be maintained throughout the operation. - /// - private List? referencePathTypes; - protected override void OnSelectionChanged() { base.OnSelectionChanged(); From 5f8512896e0eee0eda8ec0c9410704422d475182 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 12:40:22 +0200 Subject: [PATCH 025/308] use grid origin in scale tool --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 8 +++++--- osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index a299eebbce..65a07e2e2f 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; @@ -20,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Edit { private readonly OsuSelectionScaleHandler scaleHandler; + private readonly OsuGridToolboxGroup gridToolbox; + private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); private SliderWithTextBoxInput scaleInput = null!; @@ -32,9 +33,10 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuCheckbox xCheckBox = null!; private OsuCheckbox yCheckBox = null!; - public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler) + public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox) { this.scaleHandler = scaleHandler; + this.gridToolbox = gridToolbox; AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight }; } @@ -179,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit updateAxisCheckBoxesEnabled(); } - private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null; + private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null; private void setAxis(bool x, bool y) { diff --git a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs index 2e4d7e8b91..a41412cbe3 100644 --- a/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs +++ b/osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Edit () => new PreciseRotationPopover(RotationHandler, GridToolbox)), scaleButton = new EditorToolButton("Scale", () => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, - () => new PreciseScalePopover(ScaleHandler)) + () => new PreciseScalePopover(ScaleHandler, GridToolbox)) } }; } From d0715c5f12a98e9a31465600d1cc69bb0efe1df2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 16:23:19 +0200 Subject: [PATCH 026/308] scale along rotated axis --- .../Edit/OsuSelectionScaleHandler.cs | 104 ++++++++++++++---- .../Edit/PreciseScalePopover.cs | 8 +- .../Editing/TestSceneComposeSelectBox.cs | 2 +- .../SkinEditor/SkinSelectionScaleHandler.cs | 2 +- .../Components/SelectionScaleHandler.cs | 8 +- osu.Game/Utils/GeometryUtils.cs | 4 +- 6 files changed, 97 insertions(+), 31 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index f4fd48f183..cfcf90e5f5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.Edit defaultOrigin = OriginalSurroundingQuad.Value.Centre; } - public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); @@ -94,6 +94,7 @@ namespace osu.Game.Rulesets.Osu.Edit Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null); Vector2 actualOrigin = origin ?? defaultOrigin.Value; + scale = clampScaleToAdjustAxis(scale, adjustAxis); // for the time being, allow resizing of slider paths only if the slider is // the only hit object selected. with a group selection, it's likely the user @@ -102,15 +103,15 @@ namespace osu.Game.Rulesets.Osu.Edit { var originalInfo = objectsInScale[slider]; Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null); - scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes); + scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation); } else { - scale = ClampScaleToPlayfieldBounds(scale, actualOrigin); + scale = ClampScaleToPlayfieldBounds(scale, actualOrigin, adjustAxis, axisRotation); foreach (var (ho, originalState) in objectsInScale) { - ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position); + ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation); } } @@ -134,14 +135,34 @@ namespace osu.Game.Rulesets.Osu.Edit private IEnumerable selectedMovableObjects => selectedItems.Cast() .Where(h => h is not Spinner); - private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes) + private Vector2 clampScaleToAdjustAxis(Vector2 scale, Axes adjustAxis) + { + switch (adjustAxis) + { + case Axes.Y: + scale.X = 1; + break; + + case Axes.X: + scale.Y = 1; + break; + + case Axes.None: + scale = Vector2.One; + break; + } + + return scale; + } + + private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0) { scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); // Maintain the path types in case they were defaulted to bezier at some point during scaling for (int i = 0; i < slider.Path.ControlPoints.Count; i++) { - slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale; + slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation); slider.Path.ControlPoints[i].Type = originalPathTypes[i]; } @@ -176,11 +197,13 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// The origin from which the scale operation is performed /// The scale to be clamped + /// The axes to adjust the scale in. + /// The rotation of the axes in degrees /// The clamped scale vector - public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null) + public Vector2 ClampScaleToPlayfieldBounds(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { //todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead. - if (objectsInScale == null) + if (objectsInScale == null || adjustAxis == Axes.None) return scale; Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null); @@ -188,24 +211,63 @@ namespace osu.Game.Rulesets.Osu.Edit if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) origin = slider.Position; + scale = clampScaleToAdjustAxis(scale, adjustAxis); Vector2 actualOrigin = origin ?? defaultOrigin.Value; var selectionQuad = OriginalSurroundingQuad.Value; - var tl1 = Vector2.Divide(-actualOrigin, selectionQuad.TopLeft - actualOrigin); - var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.TopLeft - actualOrigin); - var br1 = Vector2.Divide(-actualOrigin, selectionQuad.BottomRight - actualOrigin); - var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - actualOrigin, selectionQuad.BottomRight - actualOrigin); - - if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - actualOrigin.X, 0)) - scale.X = selectionQuad.TopLeft.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X); - if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - actualOrigin.Y, 0)) - scale.Y = selectionQuad.TopLeft.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y); - if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - actualOrigin.X, 0)) - scale.X = selectionQuad.BottomRight.X - actualOrigin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X); - if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - actualOrigin.Y, 0)) - scale.Y = selectionQuad.BottomRight.Y - actualOrigin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y); + scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.X, Axes.X); + scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.Y, Axes.Y); + scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.X); + scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.Y); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); + + Vector2 clampToBound(Vector2 s, Vector2 p, float bound, Axes axis) + { + float px = p.X - actualOrigin.X; + float py = p.Y - actualOrigin.Y; + float c = axis == Axes.X ? bound - actualOrigin.X : bound - actualOrigin.Y; + float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); + float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); + float a, b; + + if (axis == Axes.X) + { + a = cos * cos * px - sin * cos * py; + b = sin * sin * px + sin * cos * py; + } + else + { + a = -sin * cos * px + sin * sin * py; + b = sin * cos * px + cos * cos * py; + } + + switch (adjustAxis) + { + case Axes.X: + if (Precision.AlmostEquals(a, 0) || (c - b) / a < 0) + break; + + s.X = MathF.Min(scale.X, (c - b) / a); + break; + + case Axes.Y: + if (Precision.AlmostEquals(b, 0) || (c - a) / b < 0) + break; + + s.Y = MathF.Min(scale.Y, (c - a) / b); + break; + + case Axes.Both: + if (Precision.AlmostEquals(a + b, 0) || c / (a * s.X + b * s.Y) < 0) + break; + + s = Vector2.ComponentMin(s, s * c / (a * s.X + b * s.Y)); + break; + } + + return s; + } } private void moveSelectionInBounds() diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 65a07e2e2f..a1907a2fd5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -130,8 +130,8 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInfo.BindValueChanged(scale => { - var newScale = new Vector2(scale.NewValue.XAxis ? scale.NewValue.Scale : 1, scale.NewValue.YAxis ? scale.NewValue.Scale : 1); - scaleHandler.Update(newScale, getOriginPosition(scale.NewValue)); + var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale); + scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), gridToolbox.GridLinesRotation.Value); }); } @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; const float max_scale = 10; - var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value)); + var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), gridToolbox.GridLinesRotation.Value); if (!scaleInfo.Value.XAxis) scale.X = max_scale; @@ -183,6 +183,8 @@ namespace osu.Game.Rulesets.Osu.Edit private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null; + private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; + private void setAxis(bool x, bool y) { scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y }; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 28763051e3..30f397f518 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height); } - public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (targetContainer == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 4bfa7fba81..977aaade99 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -73,7 +73,7 @@ namespace osu.Game.Overlays.SkinEditor isFlippedY = false; } - public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (objectsInScale == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!"); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs index c91362219c..177de9df33 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionScaleHandler.cs @@ -52,10 +52,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// If the default value is supplied, a sane implementation-defined default will be used. /// /// The axes to adjust the scale in. - public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + /// The rotation of the axes in degrees. + public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { Begin(); - Update(scale, origin, adjustAxis); + Update(scale, origin, adjustAxis, axisRotation); Commit(); } @@ -91,7 +92,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// If the default value is supplied, a sane implementation-defined default will be used. /// /// The axes to adjust the scale in. - public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both) + /// The rotation of the axes in degrees. + public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index bf1addf6c8..f6e7e81007 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -104,9 +104,9 @@ namespace osu.Game.Utils /// Given a scale multiplier, an origin, and a position, /// will return the scaled position in screen space coordinates. /// - public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position) + public static Vector2 GetScaledPosition(Vector2 scale, Vector2 origin, Vector2 position, float axisRotation = 0) { - return origin + (position - origin) * scale; + return origin + RotateVector(RotateVector(position - origin, axisRotation) * scale, -axisRotation); } /// From 979a5e9f3e5de341bcfe785ca7978f5449e0fd78 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 16:41:41 +0200 Subject: [PATCH 027/308] simplify code --- .../Edit/OsuSelectionScaleHandler.cs | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index cfcf90e5f5..2cf5a604ed 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. - /// + /// The origin from which the scale operation is performed /// The scale to be clamped /// The axes to adjust the scale in. @@ -215,54 +215,34 @@ namespace osu.Game.Rulesets.Osu.Edit Vector2 actualOrigin = origin ?? defaultOrigin.Value; var selectionQuad = OriginalSurroundingQuad.Value; - scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.X, Axes.X); - scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE.Y, Axes.Y); - scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.X); - scale = clampToBound(scale, selectionQuad.TopLeft, 0, Axes.Y); + scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE); + scale = clampToBound(scale, selectionQuad.TopLeft, Vector2.Zero); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); - Vector2 clampToBound(Vector2 s, Vector2 p, float bound, Axes axis) + float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y); + + Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound) { - float px = p.X - actualOrigin.X; - float py = p.Y - actualOrigin.Y; - float c = axis == Axes.X ? bound - actualOrigin.X : bound - actualOrigin.Y; + p -= actualOrigin; + bound -= actualOrigin; float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); - float a, b; - - if (axis == Axes.X) - { - a = cos * cos * px - sin * cos * py; - b = sin * sin * px + sin * cos * py; - } - else - { - a = -sin * cos * px + sin * sin * py; - b = sin * cos * px + cos * cos * py; - } + var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); + var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); switch (adjustAxis) { case Axes.X: - if (Precision.AlmostEquals(a, 0) || (c - b) / a < 0) - break; - - s.X = MathF.Min(scale.X, (c - b) / a); + s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a))); break; case Axes.Y: - if (Precision.AlmostEquals(b, 0) || (c - a) / b < 0) - break; - - s.Y = MathF.Min(scale.Y, (c - a) / b); + s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); break; case Axes.Both: - if (Precision.AlmostEquals(a + b, 0) || c / (a * s.X + b * s.Y) < 0) - break; - - s = Vector2.ComponentMin(s, s * c / (a * s.X + b * s.Y)); + s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y))); break; } From 0797d942aecde4e267fca11fbec5bf73611fc9b8 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 16:41:57 +0200 Subject: [PATCH 028/308] fix warning --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 2cf5a604ed..8b87246456 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip. - /// /// The origin from which the scale operation is performed /// The scale to be clamped /// The axes to adjust the scale in. From 4165ded8134d05f4d6b934255a5678a6a7d74bca Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 19:03:15 +0200 Subject: [PATCH 029/308] fix incorrect rotated bound checking --- .../Edit/OsuSelectionScaleHandler.cs | 53 +++++++++++++++---- osu.Game/Utils/GeometryUtils.cs | 20 ++++--- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 8b87246456..d336261499 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -80,12 +80,32 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); - OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider - ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) - : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); + OriginalSurroundingQuad = getOriginalSurroundingQuad()!; defaultOrigin = OriginalSurroundingQuad.Value.Centre; } + private Quad? getOriginalSurroundingQuad(float axisRotation = 0) + { + if (objectsInScale == null) + return null; + + return objectsInScale.Count == 1 && objectsInScale.First().Value.PathControlPointPositions != null + ? GeometryUtils.GetSurroundingQuad(objectsInScale.First().Value.PathControlPointPositions!.Select(p => objectsInScale.First().Value.Position + p), axisRotation) + : GeometryUtils.GetSurroundingQuad(objectsInScale.Values.SelectMany(s => + { + if (s.EndPosition.HasValue) + { + return new[] + { + s.Position, + s.Position + s.EndPosition.Value + }; + } + + return new[] { s.Position }; + }), axisRotation); + } + public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (!OperationInProgress.Value) @@ -213,10 +233,23 @@ namespace osu.Game.Rulesets.Osu.Edit scale = clampScaleToAdjustAxis(scale, adjustAxis); Vector2 actualOrigin = origin ?? defaultOrigin.Value; - var selectionQuad = OriginalSurroundingQuad.Value; + var selectionQuad = axisRotation == 0 ? OriginalSurroundingQuad.Value : getOriginalSurroundingQuad(axisRotation)!.Value; + var points = new[] + { + selectionQuad.TopLeft, + selectionQuad.TopRight, + selectionQuad.BottomLeft, + selectionQuad.BottomRight + }; - scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE); - scale = clampToBound(scale, selectionQuad.TopLeft, Vector2.Zero); + float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); + float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); + + foreach (var point in points) + { + scale = clampToBound(scale, point, Vector2.Zero); + scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE); + } return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); @@ -226,19 +259,17 @@ namespace osu.Game.Rulesets.Osu.Edit { p -= actualOrigin; bound -= actualOrigin; - float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); - float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); switch (adjustAxis) { case Axes.X: - s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a))); + s.X = MathF.Min(s.X, minPositiveComponent(Vector2.Divide(bound - b, a))); break; case Axes.Y: - s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); + s.Y = MathF.Min(s.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); break; case Axes.Both: @@ -275,12 +306,14 @@ namespace osu.Game.Rulesets.Osu.Edit public Vector2 Position { get; } public Vector2[]? PathControlPointPositions { get; } public PathType?[]? PathControlPointTypes { get; } + public Vector2? EndPosition { get; } public OriginalHitObjectState(OsuHitObject hitObject) { Position = hitObject.Position; PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray(); PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray(); + EndPosition = (hitObject as IHasPath)?.Path.PositionAt(1); } } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index f6e7e81007..23c25cfffa 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -113,7 +113,8 @@ namespace osu.Game.Utils /// Returns a quad surrounding the provided points. /// /// The points to calculate a quad for. - public static Quad GetSurroundingQuad(IEnumerable points) + /// The rotation in degrees of the axis to align the quad to. + public static Quad GetSurroundingQuad(IEnumerable points, float axisRotation = 0) { if (!points.Any()) return new Quad(); @@ -124,20 +125,25 @@ namespace osu.Game.Utils // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted foreach (var p in points) { - minPosition = Vector2.ComponentMin(minPosition, p); - maxPosition = Vector2.ComponentMax(maxPosition, p); + var pr = RotateVector(p, axisRotation); + minPosition = Vector2.ComponentMin(minPosition, pr); + maxPosition = Vector2.ComponentMax(maxPosition, pr); } - Vector2 size = maxPosition - minPosition; + var p1 = RotateVector(minPosition, -axisRotation); + var p2 = RotateVector(new Vector2(minPosition.X, maxPosition.Y), -axisRotation); + var p3 = RotateVector(maxPosition, -axisRotation); + var p4 = RotateVector(new Vector2(maxPosition.X, minPosition.Y), -axisRotation); - return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); + return new Quad(p1, p2, p3, p4); } /// /// Returns a gamefield-space quad surrounding the provided hit objects. /// /// The hit objects to calculate a quad for. - public static Quad GetSurroundingQuad(IEnumerable hitObjects) => + /// The rotation in degrees of the axis to align the quad to. + public static Quad GetSurroundingQuad(IEnumerable hitObjects, float axisRotation = 0) => GetSurroundingQuad(hitObjects.SelectMany(h => { if (h is IHasPath path) @@ -151,6 +157,6 @@ namespace osu.Game.Utils } return new[] { h.Position }; - })); + }), axisRotation); } } From dfe6c70996b06fa0e597fffbf1aec75e5b1d508c Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 3 Jul 2024 19:08:31 +0200 Subject: [PATCH 030/308] prevent flipping objects far offscreen --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 7720afe60a..7d6ef66909 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Utils; using osuTK; @@ -119,6 +120,9 @@ namespace osu.Game.Rulesets.Osu.Edit { var flippedPosition = GeometryUtils.GetFlippedPosition(flipAxis, flipQuad, h.Position); + // Clamp the flipped position inside the playfield bounds, because the flipped position might be outside the playfield bounds if the origin is not centered. + flippedPosition = Vector2.Clamp(flippedPosition, Vector2.Zero, OsuPlayfield.BASE_SIZE); + if (!Precision.AlmostEquals(flippedPosition, h.Position)) { h.Position = flippedPosition; From fae8f5f81b4d2de10fe1e2f2e58f0258158a794b Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:28:49 -0400 Subject: [PATCH 031/308] Refactor VeloUpdateManager --- osu.Desktop/Updater/VeloUpdateManager.cs | 49 +++++++++++------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/osu.Desktop/Updater/VeloUpdateManager.cs b/osu.Desktop/Updater/VeloUpdateManager.cs index 8fc68f77cd..137e48f135 100644 --- a/osu.Desktop/Updater/VeloUpdateManager.cs +++ b/osu.Desktop/Updater/VeloUpdateManager.cs @@ -10,12 +10,13 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; using osu.Game.Updater; +using Velopack.Sources; namespace osu.Desktop.Updater { public partial class VeloUpdateManager : UpdateManager { - private Velopack.UpdateManager? updateManager; + private readonly Velopack.UpdateManager updateManager; private INotificationOverlay notificationOverlay = null!; [Resolved] @@ -24,6 +25,12 @@ namespace osu.Desktop.Updater [Resolved] private ILocalUserPlayInfo? localUserInfo { get; set; } + public VeloUpdateManager() + { + const string? github_token = null; // TODO: populate. + updateManager = new Velopack.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false)); + } + [BackgroundDependencyLoader] private void load(INotificationOverlay notifications) { @@ -37,36 +44,30 @@ namespace osu.Desktop.Updater // should we schedule a retry on completion of this check? bool scheduleRecheck = true; - const string? github_token = null; // TODO: populate. - try { // Avoid any kind of update checking while gameplay is running. if (localUserInfo?.IsPlaying.Value == true) return false; - updateManager ??= new Velopack.UpdateManager(new Velopack.Sources.GithubSource(@"https://github.com/ppy/osu", github_token, false)); - var info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + // Handle no updates available. if (info == null) { - // If there is an update pending restart, show the notification again. - if (updateManager.IsUpdatePendingRestart) - { - notificationOverlay.Post(new UpdateApplicationCompleteNotification - { - Activated = () => - { - restartToApplyUpdate(); - return true; - } - }); - return true; - } + // If there's no updates pending restart, bail and retry later. + if (!updateManager.IsUpdatePendingRestart) return false; - // Otherwise there's no updates available. Bail and retry later. - return false; + // If there is an update pending restart, show the notification to restart again. + notificationOverlay.Post(new UpdateApplicationCompleteNotification + { + Activated = () => + { + restartToApplyUpdate(); + return true; + } + }); + return true; } scheduleRecheck = false; @@ -87,8 +88,6 @@ namespace osu.Desktop.Updater { await updateManager.DownloadUpdatesAsync(info, p => notification.Progress = p / 100f).ConfigureAwait(false); - notification.StartInstall(); - notification.State = ProgressNotificationState.Completed; } catch (Exception e) @@ -98,10 +97,11 @@ namespace osu.Desktop.Updater Logger.Error(e, @"update failed!"); } } - catch (Exception) + catch (Exception e) { // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion. scheduleRecheck = true; + Logger.Error(e, @"update check failed!"); } finally { @@ -117,9 +117,6 @@ namespace osu.Desktop.Updater private bool restartToApplyUpdate() { - if (updateManager == null) - return false; - updateManager.WaitExitThenApplyUpdates(null); Schedule(() => game.AttemptExit()); return true; From cae3607caf0d9322497c7be8c0bab4a9d2314cee Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:30:42 -0400 Subject: [PATCH 032/308] Fix up restarting Earlier I changed the restarting logic to not wait until the program exits and instead try to facilitate restarting alone. This did not work, and it became clear we'd need Velopack to do the restarting. This reverts back and supposedly brings restarting logic in line with how Velopack does it --- osu.Desktop/OsuGameDesktop.cs | 13 +++---------- .../Screens/Setup/TournamentSwitcher.cs | 3 ++- osu.Game/OsuGameBase.cs | 2 +- .../Settings/Sections/Graphics/RendererSettings.cs | 3 ++- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 28f3d3dc5d..c0c8d1e504 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.Versioning; @@ -104,23 +103,17 @@ namespace osu.Desktop return new VeloUpdateManager(); } - public override bool RestartApp() + public override bool RestartAppWhenExited() { try { - var startInfo = new ProcessStartInfo - { - FileName = Process.GetCurrentProcess().MainModule!.FileName, - UseShellExecute = true - }; - Process.Start(startInfo); - base.AttemptExit(); + Velopack.UpdateExe.Start(null, true); return true; } catch (Exception e) { Logger.Error(e, "Failed to restart application"); - return base.RestartApp(); + return base.RestartAppWhenExited(); } } diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index 69ead451ba..e55cbc2dbb 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -31,7 +31,8 @@ namespace osu.Game.Tournament.Screens.Setup Action = () => { - game.RestartApp(); + game.RestartAppWhenExited(); + game.AttemptExit(); }; folderButton.Action = () => storage.PresentExternally(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 573695613d..5e4ec5a61d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -513,7 +513,7 @@ namespace osu.Game /// If supported by the platform, the game will automatically restart after the next exit. /// /// Whether a restart operation was queued. - public virtual bool RestartApp() => false; + public virtual bool RestartAppWhenExited() => false; public bool Migrate(string path) { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 17e345c2c8..a8b127d522 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -67,8 +67,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (r.NewValue == RendererType.Automatic && automaticRendererInUse) return; - if (game?.RestartApp() == true) + if (game?.RestartAppWhenExited() == true) { + game.AttemptExit(); } else { From c13f24d553a829b0d55ccdad733cee43d1509b5d Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:32:30 -0400 Subject: [PATCH 033/308] Remove InstallingUpdate progress notification Velopack won't install the updates while the program is open, it'll do it in between restarts or before starting. --- osu.Game/Localisation/NotificationsStrings.cs | 5 ----- osu.Game/Updater/UpdateManager.cs | 6 ------ 2 files changed, 11 deletions(-) diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs index 698fe230b2..d8f768f2d8 100644 --- a/osu.Game/Localisation/NotificationsStrings.cs +++ b/osu.Game/Localisation/NotificationsStrings.cs @@ -135,11 +135,6 @@ Click to see what's new!", version); /// public static LocalisableString DownloadingUpdate => new TranslatableString(getKey(@"downloading_update"), @"Downloading update..."); - /// - /// "Installing update..." - /// - public static LocalisableString InstallingUpdate => new TranslatableString(getKey(@"installing_update"), @"Installing update..."); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index bcb28d8b14..c114e3a8d0 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -176,12 +176,6 @@ namespace osu.Game.Updater Text = NotificationsStrings.DownloadingUpdate; } - public void StartInstall() - { - Progress = 0; - Text = NotificationsStrings.InstallingUpdate; - } - public void FailDownload() { State = ProgressNotificationState.Cancelled; From 461b791532ee9abebebc615d2e23f5a38f7324c8 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:32:56 -0400 Subject: [PATCH 034/308] Remove SimpleUpdateManager No longer needed. Velopack supports the platforms that this covers for --- osu.Game/Updater/SimpleUpdateManager.cs | 116 ------------------------ 1 file changed, 116 deletions(-) delete mode 100644 osu.Game/Updater/SimpleUpdateManager.cs diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs deleted file mode 100644 index 0f9d5b929f..0000000000 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using osu.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Platform; -using osu.Game.Online.API; -using osu.Game.Overlays.Notifications; - -namespace osu.Game.Updater -{ - /// - /// An update manager that shows notifications if a newer release is detected. - /// Installation is left up to the user. - /// - public partial class SimpleUpdateManager : UpdateManager - { - private string version = null!; - - [Resolved] - private GameHost host { get; set; } = null!; - - [BackgroundDependencyLoader] - private void load(OsuGameBase game) - { - version = game.Version; - } - - protected override async Task PerformUpdateCheck() - { - try - { - var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest"); - - await releases.PerformAsync().ConfigureAwait(false); - - var latest = releases.ResponseObject; - - // avoid any discrepancies due to build suffixes for now. - // eventually we will want to support release streams and consider these. - version = version.Split('-').First(); - string latestTagName = latest.TagName.Split('-').First(); - - if (latestTagName != version && tryGetBestUrl(latest, out string? url)) - { - Notifications.Post(new SimpleNotification - { - Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" - + "Click here to download the new version, which can be installed over the top of your existing installation", - Icon = FontAwesome.Solid.Download, - Activated = () => - { - host.OpenUrlExternally(url); - return true; - } - }); - - return true; - } - } - catch - { - // we shouldn't crash on a web failure. or any failure for the matter. - return true; - } - - return false; - } - - private bool tryGetBestUrl(GitHubRelease release, [NotNullWhen(true)] out string? url) - { - url = null; - GitHubAsset? bestAsset = null; - - switch (RuntimeInfo.OS) - { - case RuntimeInfo.Platform.Windows: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal)); - break; - - case RuntimeInfo.Platform.macOS: - string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "Apple.Silicon" : "Intel"; - bestAsset = release.Assets?.Find(f => f.Name.EndsWith($".app.{arch}.zip", StringComparison.Ordinal)); - break; - - case RuntimeInfo.Platform.Linux: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage", StringComparison.Ordinal)); - break; - - case RuntimeInfo.Platform.iOS: - if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true) - // iOS releases are available via testflight. this link seems to work well enough for now. - // see https://stackoverflow.com/a/32960501 - url = "itms-beta://beta.itunes.apple.com/v1/app/1447765923"; - - break; - - case RuntimeInfo.Platform.Android: - if (release.Assets?.Exists(f => f.Name.EndsWith(".apk", StringComparison.Ordinal)) == true) - // on our testing device using the .apk URL causes the download to magically disappear. - url = release.HtmlUrl; - - break; - } - - url ??= bestAsset?.BrowserDownloadUrl; - return url != null; - } - } -} From 9e01cf7fc23812c4d0a76b09dd78ce6b38c06028 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:33:05 -0400 Subject: [PATCH 035/308] Move setupVelo logic higher up --- osu.Desktop/Program.cs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 7c23c15d5a..92c8f2104c 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -30,19 +30,9 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { - /* - * WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK! - * - * Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it. - * To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit, - * namely by checking loaded assemblies: - * https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32 - * - * If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded - - * the app will then do completely broken things like: - * - not creating system shortcuts (as the logic is if'd out if "running tests") - * - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests") - */ + // Velopack needs to run before anything else + setupVelo(); + if (OperatingSystem.IsWindows()) { var windowsVersion = Environment.OSVersion.Version; @@ -67,8 +57,6 @@ namespace osu.Desktop } } - setupVelo(); - // NVIDIA profiles are based on the executable name of a process. // Lazer and stable share the same executable name. // Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup. From 6a0309294440dfd7136d2ceed72ca19e1be951eb Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 17:45:34 -0400 Subject: [PATCH 036/308] Reformat --- osu.Desktop/Updater/VeloUpdateManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/Updater/VeloUpdateManager.cs b/osu.Desktop/Updater/VeloUpdateManager.cs index 137e48f135..8aa580caa8 100644 --- a/osu.Desktop/Updater/VeloUpdateManager.cs +++ b/osu.Desktop/Updater/VeloUpdateManager.cs @@ -92,9 +92,9 @@ namespace osu.Desktop.Updater } catch (Exception e) { - // In the case of an error, a separate notification will be displayed. - notification.FailDownload(); - Logger.Error(e, @"update failed!"); + // In the case of an error, a separate notification will be displayed. + notification.FailDownload(); + Logger.Error(e, @"update failed!"); } } catch (Exception e) From 72cf6bb12c71405464ce606a0334b2d6836c1bbc Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 18:00:45 -0400 Subject: [PATCH 037/308] Allow downgrading Also better address UpdateManager conflict --- osu.Desktop/Updater/VeloUpdateManager.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/Updater/VeloUpdateManager.cs b/osu.Desktop/Updater/VeloUpdateManager.cs index 8aa580caa8..6d3eb3f3f0 100644 --- a/osu.Desktop/Updater/VeloUpdateManager.cs +++ b/osu.Desktop/Updater/VeloUpdateManager.cs @@ -9,14 +9,15 @@ using osu.Game; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; -using osu.Game.Updater; +using Velopack; using Velopack.Sources; +using UpdateManager = Velopack.UpdateManager; namespace osu.Desktop.Updater { - public partial class VeloUpdateManager : UpdateManager + public partial class VeloUpdateManager : Game.Updater.UpdateManager { - private readonly Velopack.UpdateManager updateManager; + private readonly UpdateManager updateManager; private INotificationOverlay notificationOverlay = null!; [Resolved] @@ -28,7 +29,10 @@ namespace osu.Desktop.Updater public VeloUpdateManager() { const string? github_token = null; // TODO: populate. - updateManager = new Velopack.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false)); + updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), new UpdateOptions + { + AllowVersionDowngrade = true + }); } [BackgroundDependencyLoader] From 4898cff7a4186c3202cdef540c2480b48e22d55d Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Thu, 4 Jul 2024 18:25:02 -0400 Subject: [PATCH 038/308] Restart patch --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Desktop/osu.Desktop.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index c0c8d1e504..ee73c84ba3 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -107,7 +107,7 @@ namespace osu.Desktop { try { - Velopack.UpdateExe.Start(null, true); + Velopack.UpdateExe.Start(); return true; } catch (Exception e) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 7df82e1281..7a2bb599fd 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From 71816c09dccf0a17932acb647e749190371e84a0 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Fri, 5 Jul 2024 03:29:09 -0400 Subject: [PATCH 039/308] Resurrect SimpleUpdateManager as MobileUpdateNotifier While removing the desktop specific logic from it --- osu.Android/OsuGameAndroid.cs | 2 +- osu.Game/Updater/MobileUpdateNotifier.cs | 102 +++++++++++++++++++++++ osu.iOS/OsuGameIOS.cs | 2 +- 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Updater/MobileUpdateNotifier.cs diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index a235913ef3..ffab7dd86d 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -80,7 +80,7 @@ namespace osu.Android host.Window.CursorState |= CursorState.Hidden; } - protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); diff --git a/osu.Game/Updater/MobileUpdateNotifier.cs b/osu.Game/Updater/MobileUpdateNotifier.cs new file mode 100644 index 0000000000..04b54df3c0 --- /dev/null +++ b/osu.Game/Updater/MobileUpdateNotifier.cs @@ -0,0 +1,102 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Platform; +using osu.Game.Online.API; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Updater +{ + /// + /// An update manager that shows notifications if a newer release is detected for mobile platforms. + /// Installation is left up to the user. + /// + public partial class MobileUpdateNotifier : UpdateManager + { + private string version = null!; + + [Resolved] + private GameHost host { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuGameBase game) + { + version = game.Version; + } + + protected override async Task PerformUpdateCheck() + { + try + { + var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest"); + + await releases.PerformAsync().ConfigureAwait(false); + + var latest = releases.ResponseObject; + + // avoid any discrepancies due to build suffixes for now. + // eventually we will want to support release streams and consider these. + version = version.Split('-').First(); + string latestTagName = latest.TagName.Split('-').First(); + + if (latestTagName != version && tryGetBestUrl(latest, out string? url)) + { + Notifications.Post(new SimpleNotification + { + Text = $"A newer release of osu! has been found ({version} → {latestTagName}).\n\n" + + "Click here to download the new version, which can be installed over the top of your existing installation", + Icon = FontAwesome.Solid.Download, + Activated = () => + { + host.OpenUrlExternally(url); + return true; + } + }); + + return true; + } + } + catch + { + // we shouldn't crash on a web failure. or any failure for the matter. + return true; + } + + return false; + } + + private bool tryGetBestUrl(GitHubRelease release, [NotNullWhen(true)] out string? url) + { + url = null; + GitHubAsset? bestAsset = null; + + switch (RuntimeInfo.OS) + { + case RuntimeInfo.Platform.iOS: + if (release.Assets?.Exists(f => f.Name.EndsWith(".ipa", StringComparison.Ordinal)) == true) + // iOS releases are available via testflight. this link seems to work well enough for now. + // see https://stackoverflow.com/a/32960501 + url = "itms-beta://beta.itunes.apple.com/v1/app/1447765923"; + + break; + + case RuntimeInfo.Platform.Android: + if (release.Assets?.Exists(f => f.Name.EndsWith(".apk", StringComparison.Ordinal)) == true) + // on our testing device using the .apk URL causes the download to magically disappear. + url = release.HtmlUrl; + + break; + } + + url ??= bestAsset?.BrowserDownloadUrl; + return url != null; + } + } +} diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 502f302157..2a4f9b87ac 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -15,7 +15,7 @@ namespace osu.iOS { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); - protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); + protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); From ae380027772867b45baf80acc910c98cb39fac46 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 15:46:40 +0200 Subject: [PATCH 040/308] Revert "fix incorrect rotated bound checking" This reverts commit 4165ded8134d05f4d6b934255a5678a6a7d74bca. --- .../Edit/OsuSelectionScaleHandler.cs | 53 ++++--------------- osu.Game/Utils/GeometryUtils.cs | 20 +++---- 2 files changed, 17 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index d336261499..8b87246456 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -80,32 +80,12 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho)); - OriginalSurroundingQuad = getOriginalSurroundingQuad()!; + OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider + ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) + : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); defaultOrigin = OriginalSurroundingQuad.Value.Centre; } - private Quad? getOriginalSurroundingQuad(float axisRotation = 0) - { - if (objectsInScale == null) - return null; - - return objectsInScale.Count == 1 && objectsInScale.First().Value.PathControlPointPositions != null - ? GeometryUtils.GetSurroundingQuad(objectsInScale.First().Value.PathControlPointPositions!.Select(p => objectsInScale.First().Value.Position + p), axisRotation) - : GeometryUtils.GetSurroundingQuad(objectsInScale.Values.SelectMany(s => - { - if (s.EndPosition.HasValue) - { - return new[] - { - s.Position, - s.Position + s.EndPosition.Value - }; - } - - return new[] { s.Position }; - }), axisRotation); - } - public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) { if (!OperationInProgress.Value) @@ -233,23 +213,10 @@ namespace osu.Game.Rulesets.Osu.Edit scale = clampScaleToAdjustAxis(scale, adjustAxis); Vector2 actualOrigin = origin ?? defaultOrigin.Value; - var selectionQuad = axisRotation == 0 ? OriginalSurroundingQuad.Value : getOriginalSurroundingQuad(axisRotation)!.Value; - var points = new[] - { - selectionQuad.TopLeft, - selectionQuad.TopRight, - selectionQuad.BottomLeft, - selectionQuad.BottomRight - }; + var selectionQuad = OriginalSurroundingQuad.Value; - float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); - float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); - - foreach (var point in points) - { - scale = clampToBound(scale, point, Vector2.Zero); - scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE); - } + scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE); + scale = clampToBound(scale, selectionQuad.TopLeft, Vector2.Zero); return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); @@ -259,17 +226,19 @@ namespace osu.Game.Rulesets.Osu.Edit { p -= actualOrigin; bound -= actualOrigin; + float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); + float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); switch (adjustAxis) { case Axes.X: - s.X = MathF.Min(s.X, minPositiveComponent(Vector2.Divide(bound - b, a))); + s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a))); break; case Axes.Y: - s.Y = MathF.Min(s.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); + s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b))); break; case Axes.Both: @@ -306,14 +275,12 @@ namespace osu.Game.Rulesets.Osu.Edit public Vector2 Position { get; } public Vector2[]? PathControlPointPositions { get; } public PathType?[]? PathControlPointTypes { get; } - public Vector2? EndPosition { get; } public OriginalHitObjectState(OsuHitObject hitObject) { Position = hitObject.Position; PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray(); PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray(); - EndPosition = (hitObject as IHasPath)?.Path.PositionAt(1); } } } diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 23c25cfffa..f6e7e81007 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -113,8 +113,7 @@ namespace osu.Game.Utils /// Returns a quad surrounding the provided points. /// /// The points to calculate a quad for. - /// The rotation in degrees of the axis to align the quad to. - public static Quad GetSurroundingQuad(IEnumerable points, float axisRotation = 0) + public static Quad GetSurroundingQuad(IEnumerable points) { if (!points.Any()) return new Quad(); @@ -125,25 +124,20 @@ namespace osu.Game.Utils // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted foreach (var p in points) { - var pr = RotateVector(p, axisRotation); - minPosition = Vector2.ComponentMin(minPosition, pr); - maxPosition = Vector2.ComponentMax(maxPosition, pr); + minPosition = Vector2.ComponentMin(minPosition, p); + maxPosition = Vector2.ComponentMax(maxPosition, p); } - var p1 = RotateVector(minPosition, -axisRotation); - var p2 = RotateVector(new Vector2(minPosition.X, maxPosition.Y), -axisRotation); - var p3 = RotateVector(maxPosition, -axisRotation); - var p4 = RotateVector(new Vector2(maxPosition.X, minPosition.Y), -axisRotation); + Vector2 size = maxPosition - minPosition; - return new Quad(p1, p2, p3, p4); + return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); } /// /// Returns a gamefield-space quad surrounding the provided hit objects. /// /// The hit objects to calculate a quad for. - /// The rotation in degrees of the axis to align the quad to. - public static Quad GetSurroundingQuad(IEnumerable hitObjects, float axisRotation = 0) => + public static Quad GetSurroundingQuad(IEnumerable hitObjects) => GetSurroundingQuad(hitObjects.SelectMany(h => { if (h is IHasPath path) @@ -157,6 +151,6 @@ namespace osu.Game.Utils } return new[] { h.Position }; - }), axisRotation); + })); } } From 58eb7f6fe174ba72f06304d0334b0668dde14c74 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 16:58:05 +0200 Subject: [PATCH 041/308] fix rotated scale bounds again --- .../Edit/OsuSelectionScaleHandler.cs | 31 ++++++++++-- osu.Game/Utils/GeometryUtils.cs | 49 ++++++++++++++++++- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 8b87246456..56c3ba9315 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -69,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit private Dictionary? objectsInScale; private Vector2? defaultOrigin; + private List? originalConvexHull; public override void Begin() { @@ -84,6 +85,9 @@ namespace osu.Game.Rulesets.Osu.Edit ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); defaultOrigin = OriginalSurroundingQuad.Value.Centre; + originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2 + ? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position)) + : GeometryUtils.GetConvexHull(objectsInScale.Keys); } public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) @@ -211,12 +215,31 @@ namespace osu.Game.Rulesets.Osu.Edit if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) origin = slider.Position; + float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); + float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); scale = clampScaleToAdjustAxis(scale, adjustAxis); Vector2 actualOrigin = origin ?? defaultOrigin.Value; - var selectionQuad = OriginalSurroundingQuad.Value; + IEnumerable points; - scale = clampToBound(scale, selectionQuad.BottomRight, OsuPlayfield.BASE_SIZE); - scale = clampToBound(scale, selectionQuad.TopLeft, Vector2.Zero); + if (axisRotation == 0) + { + var selectionQuad = OriginalSurroundingQuad.Value; + points = new[] + { + selectionQuad.TopLeft, + selectionQuad.TopRight, + selectionQuad.BottomLeft, + selectionQuad.BottomRight + }; + } + else + points = originalConvexHull!; + + foreach (var point in points) + { + scale = clampToBound(scale, point, Vector2.Zero); + scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE); + } return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); @@ -226,8 +249,6 @@ namespace osu.Game.Rulesets.Osu.Edit { p -= actualOrigin; bound -= actualOrigin; - float cos = MathF.Cos(float.DegreesToRadians(-axisRotation)); - float sin = MathF.Sin(float.DegreesToRadians(-axisRotation)); var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y); var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y); diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index f6e7e81007..5a8ca9722e 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -138,7 +138,52 @@ namespace osu.Game.Utils /// /// The hit objects to calculate a quad for. public static Quad GetSurroundingQuad(IEnumerable hitObjects) => - GetSurroundingQuad(hitObjects.SelectMany(h => + GetSurroundingQuad(enumerateStartAndEndPositions(hitObjects)); + + /// + /// Returns the points that make up the convex hull of the provided points. + /// + /// The points to calculate a convex hull. + public static List GetConvexHull(IEnumerable points) + { + List p = points.ToList(); + + if (p.Count <= 1) + return p; + + int n = p.Count, k = 0; + List hull = new List(new Vector2[2 * n]); + + p.Sort((a, b) => a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X)); + + // Build lower hull + for (int i = 0; i < n; ++i) + { + while (k >= 2 && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) + k--; + hull[k] = p[i]; + k++; + } + + // Build upper hull + for (int i = n - 2, t = k + 1; i >= 0; i--) + { + while (k >= t && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) + k--; + hull[k] = p[i]; + k++; + } + + return hull.Take(k - 1).ToList(); + + float cross(Vector2 o, Vector2 a, Vector2 b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); + } + + public static List GetConvexHull(IEnumerable hitObjects) => + GetConvexHull(enumerateStartAndEndPositions(hitObjects)); + + private static IEnumerable enumerateStartAndEndPositions(IEnumerable hitObjects) => + hitObjects.SelectMany(h => { if (h is IHasPath path) { @@ -151,6 +196,6 @@ namespace osu.Game.Utils } return new[] { h.Position }; - })); + }); } } From 7a319a6d74ee29fbf3e7b5dbc2b7c6c9ca8e4990 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 17:03:17 +0200 Subject: [PATCH 042/308] dont rotate scale when in selection origin mode --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index a1907a2fd5..0f04efcfa5 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInfo.BindValueChanged(scale => { var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale); - scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), gridToolbox.GridLinesRotation.Value); + scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue)); }); } @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Edit return; const float max_scale = 10; - var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), gridToolbox.GridLinesRotation.Value); + var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value)); if (!scaleInfo.Value.XAxis) scale.X = max_scale; @@ -185,6 +185,8 @@ namespace osu.Game.Rulesets.Osu.Edit private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; + private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.GridLinesRotation.Value : 0; + private void setAxis(bool x, bool y) { scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y }; From 9e5d099b1b1113304294c17155f42ab4a8ea76cd Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 17:13:22 +0200 Subject: [PATCH 043/308] rename playfield centre origin to grid centre --- .../Edit/PreciseRotationPopover.cs | 12 ++++++------ .../Edit/PreciseScalePopover.cs | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 6a3e326c2b..4a1ccc4b61 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly OsuGridToolboxGroup gridToolbox; - private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre)); + private readonly Bindable rotationInfo = new Bindable(new PreciseRotationInfo(0, RotationOrigin.GridCentre)); private SliderWithTextBoxInput angleInput = null!; private EditorRadioButtonCollection rotationOrigin = null!; @@ -60,9 +60,9 @@ namespace osu.Game.Rulesets.Osu.Edit RelativeSizeAxes = Axes.X, Items = new[] { - new RadioButton("Playfield centre", - () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre }, - () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + new RadioButton("Grid centre", + () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre }, + () => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }), selectionCentreButton = new RadioButton("Selection centre", () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre }, () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { - rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null); + rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.GridCentre ? gridToolbox.StartPosition.Value : null); }); } @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Edit public enum RotationOrigin { - PlayfieldCentre, + GridCentre, SelectionCentre } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 0f04efcfa5..15ed4c59c3 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly OsuGridToolboxGroup gridToolbox; - private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.PlayfieldCentre, true, true)); + private readonly Bindable scaleInfo = new Bindable(new PreciseScaleInfo(1, ScaleOrigin.GridCentre, true, true)); private SliderWithTextBoxInput scaleInput = null!; private BindableNumber scaleInputBindable = null!; @@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Osu.Edit RelativeSizeAxes = Axes.X, Items = new[] { - playfieldCentreButton = new RadioButton("Playfield centre", - () => setOrigin(ScaleOrigin.PlayfieldCentre), - () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), + playfieldCentreButton = new RadioButton("Grid centre", + () => setOrigin(ScaleOrigin.GridCentre), + () => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }), selectionCentreButton = new RadioButton("Selection centre", () => setOrigin(ScaleOrigin.SelectionCentre), () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateAxisCheckBoxesEnabled() { - if (scaleInfo.Value.Origin == ScaleOrigin.PlayfieldCentre) + if (scaleInfo.Value.Origin == ScaleOrigin.GridCentre) { toggleAxisAvailable(xCheckBox.Current, true); toggleAxisAvailable(yCheckBox.Current, true); @@ -181,11 +181,11 @@ namespace osu.Game.Rulesets.Osu.Edit updateAxisCheckBoxesEnabled(); } - private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.StartPosition.Value : null; + private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.StartPosition.Value : null; private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; - private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.PlayfieldCentre ? gridToolbox.GridLinesRotation.Value : 0; + private float getRotation(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.GridLinesRotation.Value : 0; private void setAxis(bool x, bool y) { @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Edit public enum ScaleOrigin { - PlayfieldCentre, + GridCentre, SelectionCentre } From a80e3337860773790c88010a7592219984a31570 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 17:27:04 +0200 Subject: [PATCH 044/308] add playfield origin as third origin option --- .../Edit/PreciseRotationPopover.cs | 17 ++++++++++++- .../Edit/PreciseScalePopover.cs | 24 ++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 4a1ccc4b61..352debf500 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -8,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -63,6 +65,9 @@ namespace osu.Game.Rulesets.Osu.Edit new RadioButton("Grid centre", () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.GridCentre }, () => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }), + new RadioButton("Playfield centre", + () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre }, + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), selectionCentreButton = new RadioButton("Selection centre", () => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre }, () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) @@ -95,10 +100,19 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { - rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.GridCentre ? gridToolbox.StartPosition.Value : null); + rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue)); }); } + private Vector2? getOriginPosition(PreciseRotationInfo rotation) => + rotation.Origin switch + { + RotationOrigin.GridCentre => gridToolbox.StartPosition.Value, + RotationOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, + RotationOrigin.SelectionCentre => null, + _ => throw new ArgumentOutOfRangeException(nameof(rotation)) + }; + protected override void PopIn() { base.PopIn(); @@ -117,6 +131,7 @@ namespace osu.Game.Rulesets.Osu.Edit public enum RotationOrigin { GridCentre, + PlayfieldCentre, SelectionCentre } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 15ed4c59c3..dff370d259 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; @@ -27,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit private BindableNumber scaleInputBindable = null!; private EditorRadioButtonCollection scaleOrigin = null!; + private RadioButton gridCentreButton = null!; private RadioButton playfieldCentreButton = null!; private RadioButton selectionCentreButton = null!; @@ -68,9 +70,12 @@ namespace osu.Game.Rulesets.Osu.Edit RelativeSizeAxes = Axes.X, Items = new[] { - playfieldCentreButton = new RadioButton("Grid centre", + gridCentreButton = new RadioButton("Grid centre", () => setOrigin(ScaleOrigin.GridCentre), () => new SpriteIcon { Icon = FontAwesome.Regular.PlusSquare }), + playfieldCentreButton = new RadioButton("Playfield centre", + () => setOrigin(ScaleOrigin.PlayfieldCentre), + () => new SpriteIcon { Icon = FontAwesome.Regular.Square }), selectionCentreButton = new RadioButton("Selection centre", () => setOrigin(ScaleOrigin.SelectionCentre), () => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare }) @@ -99,6 +104,10 @@ namespace osu.Game.Rulesets.Osu.Edit }, } }; + gridCentreButton.Selected.DisabledChanged += isDisabled => + { + gridCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to grid centre." : string.Empty; + }; playfieldCentreButton.Selected.DisabledChanged += isDisabled => { playfieldCentreButton.TooltipText = isDisabled ? "The current selection cannot be scaled relative to playfield centre." : string.Empty; @@ -125,6 +134,7 @@ namespace osu.Game.Rulesets.Osu.Edit selectionCentreButton.Selected.Disabled = !(scaleHandler.CanScaleX.Value || scaleHandler.CanScaleY.Value); playfieldCentreButton.Selected.Disabled = scaleHandler.IsScalingSlider.Value && !selectionCentreButton.Selected.Disabled; + gridCentreButton.Selected.Disabled = playfieldCentreButton.Selected.Disabled; scaleOrigin.Items.First(b => !b.Selected.Disabled).Select(); @@ -137,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateAxisCheckBoxesEnabled() { - if (scaleInfo.Value.Origin == ScaleOrigin.GridCentre) + if (scaleInfo.Value.Origin != ScaleOrigin.SelectionCentre) { toggleAxisAvailable(xCheckBox.Current, true); toggleAxisAvailable(yCheckBox.Current, true); @@ -181,7 +191,14 @@ namespace osu.Game.Rulesets.Osu.Edit updateAxisCheckBoxesEnabled(); } - private Vector2? getOriginPosition(PreciseScaleInfo scale) => scale.Origin == ScaleOrigin.GridCentre ? gridToolbox.StartPosition.Value : null; + private Vector2? getOriginPosition(PreciseScaleInfo scale) => + scale.Origin switch + { + ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value, + ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, + ScaleOrigin.SelectionCentre => null, + _ => throw new ArgumentOutOfRangeException(nameof(scale)) + }; private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; @@ -211,6 +228,7 @@ namespace osu.Game.Rulesets.Osu.Edit public enum ScaleOrigin { GridCentre, + PlayfieldCentre, SelectionCentre } From 2bbaa8e43ccce0a1bf42f2c5790ad469a81a195e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sun, 14 Jul 2024 18:12:55 +0200 Subject: [PATCH 045/308] make flips grid-type aware --- .../Edit/OsuSelectionHandler.cs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 7d6ef66909..1334dbdbec 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -107,10 +107,28 @@ namespace osu.Game.Rulesets.Osu.Edit // If we're flipping over the origin, we take the grid origin position from the grid toolbox. var flipQuad = flipOverOrigin ? new Quad(gridToolbox.StartPositionX.Value, gridToolbox.StartPositionY.Value, 0, 0) : GeometryUtils.GetSurroundingQuad(hitObjects); - // If we're flipping over the origin, we take the grid rotation from the grid toolbox. - // We want to normalize the rotation angle to -45 to 45 degrees, so horizontal vs vertical flip is not mixed up by the rotation and it stays intuitive to use. - var flipAxis = flipOverOrigin ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)) : Vector2.UnitX; - flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; + Vector2 flipAxis = direction == Direction.Vertical ? Vector2.UnitY : Vector2.UnitX; + + if (flipOverOrigin) + { + // If we're flipping over the origin, we take one of the axes of the grid. + // Take the axis closest to the direction we want to flip over. + switch (gridToolbox.GridType.Value) + { + case PositionSnapGridType.Square: + flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)); + flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; + break; + + case PositionSnapGridType.Triangle: + // Hex grid has 3 axes, so you can not directly flip over one of the axes, + // however it's still possible to achieve that flip by combining multiple flips over the other axes. + flipAxis = direction == Direction.Vertical + ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 + 60)) + : GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 - 60)); + break; + } + } var controlPointFlipQuad = new Quad(); From c18814817b7d5b1f907574b6cca89f4fdc0efa53 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 16 Jul 2024 11:17:54 +0200 Subject: [PATCH 046/308] fix test --- osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs index 30e0dbbf2e..d14bd1fc87 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePreciseRotation.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor () => EditorBeatmap.HitObjects.OfType().ElementAt(1).Position, () => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200))); - AddStep("change rotation origin", () => getPopover().ChildrenOfType().ElementAt(1).TriggerClick()); + AddStep("change rotation origin", () => getPopover().ChildrenOfType().ElementAt(2).TriggerClick()); AddAssert("first object rotated 90deg around selection centre", () => EditorBeatmap.HitObjects.OfType().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200))); AddAssert("second object rotated 90deg around selection centre", From 7dc006f9bab4f22e326c5692a40a2afc5bfdc566 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 16 Jul 2024 13:19:01 +0200 Subject: [PATCH 047/308] fix horizontal flip rotation --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 1334dbdbec..2dc43deee1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Edit // however it's still possible to achieve that flip by combining multiple flips over the other axes. flipAxis = direction == Direction.Vertical ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 + 60)) - : GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 - 60)); + : GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30)); break; } } From 0a9b11d3a76445cbf56cba4f367964340df91e2a Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 15:57:02 +0300 Subject: [PATCH 048/308] removed default difficulty multiplier --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- .../Difficulty/Skills/Flashlight.cs | 4 ++-- .../Difficulty/Skills/OsuStrainSkill.cs | 12 +----------- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 3 +-- 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 3f6b22bbb1..f0be2440c1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 23.55; + private double skillMultiplier => 23.55 * 1.06; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 3d6d3f99c1..8caaae665a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.052; + private double skillMultiplier => 0.052 * 1.06; private double strainDecayBase => 0.15; private double currentStrain; @@ -41,6 +41,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return currentStrain; } - public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER; + public override double DifficultyValue() => GetCurrentStrainPeaks().Sum(); } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 4a6328010b..d7ceb63d36 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { public abstract class OsuStrainSkill : StrainSkill { - /// - /// The default multiplier applied by to the final difficulty value after all other calculations. - /// May be overridden via . - /// - public const double DEFAULT_DIFFICULTY_MULTIPLIER = 1.06; - /// /// The number of sections with the highest strains, which the peak strain reductions will apply to. /// This is done in order to decrease their impact on the overall difficulty of the map for this skill. @@ -29,10 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// protected virtual double ReducedStrainBaseline => 0.75; - /// - /// The final multiplier to be applied to after all other calculations. - /// - protected virtual double DifficultyMultiplier => DEFAULT_DIFFICULTY_MULTIPLIER; protected OsuStrainSkill(Mod[] mods) : base(mods) @@ -65,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills weight *= DecayWeight; } - return difficulty * DifficultyMultiplier; + return difficulty; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 40aac013ab..f54f135f63 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,14 +16,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1375; + private double skillMultiplier => 1375 * 1.04; private double strainDecayBase => 0.3; private double currentStrain; private double currentRhythm; protected override int ReducedSectionCount => 5; - protected override double DifficultyMultiplier => 1.04; private readonly List objectStrains = new List(); From 8431e62c470dbd1e71a27e0e22e27282344b9255 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:14:32 +0300 Subject: [PATCH 049/308] fixed CI --- osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index d7ceb63d36..c007c1abd2 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// protected virtual double ReducedStrainBaseline => 0.75; - protected OsuStrainSkill(Mod[] mods) : base(mods) { From e6fc4f67668817e041f4800d2dbe5078afd9a427 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:33:42 +0300 Subject: [PATCH 050/308] merged multipliers --- .../Difficulty/CatchDifficultyCalculator.cs | 4 ++-- .../Difficulty/Skills/Movement.cs | 2 +- .../Difficulty/ManiaDifficultyCalculator.cs | 4 ++-- .../Difficulty/ManiaPerformanceCalculator.cs | 6 ++---- .../Difficulty/TaikoDifficultyCalculator.cs | 18 ++++++++---------- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index f12c41a415..0899212b6c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.153; + private const double difficulty_multiplier = 4.59; private float halfCatcherWidth; @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty CatchDifficultyAttributes attributes = new CatchDifficultyAttributes { - StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, + StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index cfb3fe40be..54b85f1745 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private const float normalized_hitobject_radius = 41.0f; private const double direction_change_bonus = 21.0; - protected override double SkillMultiplier => 900; + protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.2; protected override double DecayWeight => 0.94; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 4190e74e51..efe27e8d6b 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty { public class ManiaDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.018; + private const double difficulty_multiplier = 0.018; private readonly bool isForCurrentRuleset; private readonly double originalOverallDifficulty; @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes { - StarRating = skills[0].DifficultyValue() * star_scaling_factor, + StarRating = skills[0].DifficultyValue() * difficulty_multiplier, Mods = mods, // In osu-stable mania, rate-adjustment mods don't affect the hit window. // This is done the way it is to introduce fractional differences in order to match osu-stable for the time being. diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index d9f9479247..9e5b81bf39 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -38,9 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); scoreAccuracy = calculateCustomAccuracy(); - // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. - // The specific number has no intrinsic meaning and can be adjusted as needed. - double multiplier = 8.0; + double multiplier = 1.0; if (score.Mods.Any(m => m is ModNoFail)) multiplier *= 0.75; @@ -59,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue(ManiaDifficultyAttributes attributes) { - double difficultyValue = Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve + double difficultyValue = 8.0 * Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve * Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 9b746d47ea..28323693d0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -21,12 +21,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double difficulty_multiplier = 1.35; - - private const double final_multiplier = 0.0625; - private const double rhythm_skill_multiplier = 0.2 * final_multiplier; - private const double colour_skill_multiplier = 0.375 * final_multiplier; - private const double stamina_skill_multiplier = 0.375 * final_multiplier; + private const double difficulty_multiplier = 0.084375; + private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier; + private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; + private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; public override int Version => 20221107; @@ -83,11 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); Stamina stamina = (Stamina)skills.First(x => x is Stamina); - double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier; - double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier; - double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier; + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; + double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; - double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier; + double combinedRating = combinedDifficultyValue(rhythm, colour, stamina); double starRating = rescale(combinedRating * 1.4); HitWindows hitWindows = new TaikoHitWindows(); From ac57cdd1b32cde5b2ce156101ac178b7d2ef0fb9 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:50:06 +0300 Subject: [PATCH 051/308] speed eval refactoring --- .../Difficulty/Evaluators/SpeedEvaluator.cs | 18 ++++++++++++++---- .../Difficulty/Skills/Speed.cs | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 2df383aaa8..ae7a2542bf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators { public static class SpeedEvaluator { - private const double single_spacing_threshold = 125; + private const double single_spacing_threshold = 125; // 1.25 circles distance between centers private const double min_speed_bonus = 75; // ~200BPM private const double speed_balancing_factor = 40; @@ -50,16 +50,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1); - // derive speedBonus for calculation + // speedBonus will be 1.0 for BPM < 200 double speedBonus = 1.0; + // Add additional scaling bonus for streams/bursts higher than 200bpm if (strainTime < min_speed_bonus) speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); double travelDistance = osuPrevObj?.TravelDistance ?? 0; - double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance); + double distance = travelDistance + osuCurrObj.MinimumJumpDistance; - return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) * doubletapness / strainTime; + // Cap distance at single_spacing_threshold + distance = Math.Min(distance, single_spacing_threshold); + + double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5); + + // Base difficulty with all bonuses + double difficulty = speedBonus * distanceBonus * 1000 / strainTime; + + // Apply penalty if there's doubletappable doubles + return difficulty * doubletapness; } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 40aac013ab..f7f081b7ea 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1375; + private double skillMultiplier => 1.375; private double strainDecayBase => 0.3; private double currentStrain; From ace1a572429216c7d1e033d33167ceee6248751b Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 16:53:06 +0300 Subject: [PATCH 052/308] Update SpeedEvaluator.cs --- osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index ae7a2542bf..37fd11391c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // Cap distance at single_spacing_threshold distance = Math.Min(distance, single_spacing_threshold); + // Max distance bonus is 2 at single_spacing_threshold double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5); // Base difficulty with all bonuses From 174f4d3ab7d86b77337eab8fb33b75081973e812 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Mon, 5 Aug 2024 17:02:37 +0300 Subject: [PATCH 053/308] fixed CI --- .../Difficulty/ManiaPerformanceCalculator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 9e5b81bf39..778d569cf2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeDifficultyValue(ManiaDifficultyAttributes attributes) { double difficultyValue = 8.0 * Math.Pow(Math.Max(attributes.StarRating - 0.15, 0.05), 2.2) // Star rating to pp curve - * Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy - * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes + * Math.Max(0, 5 * scoreAccuracy - 4) // From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy + * (1 + 0.1 * Math.Min(1, totalHits / 1500)); // Length bonus, capped at 1500 notes return difficultyValue; } From a28913af7a332737a06089fdf99826660f31e702 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Tue, 6 Aug 2024 14:47:05 +0300 Subject: [PATCH 054/308] multiplied numbers in multipliers --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index f0be2440c1..1fbe03395c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 23.55 * 1.06; + private double skillMultiplier => 24.963; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 8caaae665a..9ca6a35d3d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills hasHiddenMod = mods.Any(m => m is OsuModHidden); } - private double skillMultiplier => 0.052 * 1.06; + private double skillMultiplier => 0.05512; private double strainDecayBase => 0.15; private double currentStrain; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f54f135f63..93e6e2d1e4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1375 * 1.04; + private double skillMultiplier => 1430; private double strainDecayBase => 0.3; private double currentStrain; From fcede9abd786854bc0858fe90db2cbdf320a56ac Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Wed, 7 Aug 2024 03:34:07 -0400 Subject: [PATCH 055/308] Bump velopack version --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 7a2bb599fd..d86fcec396 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From b18706274784430b26526fc4b3eecb75c8ef058e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 13 Aug 2024 12:58:52 +0200 Subject: [PATCH 056/308] clarify meaning of flip axis vector --- osu.Game/Utils/GeometryUtils.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 5a8ca9722e..810eda9950 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -51,6 +51,9 @@ namespace osu.Game.Utils /// Given a flip direction, a surrounding quad for all selected objects, and a position, /// will return the flipped position in screen space coordinates. /// + /// The direction to flip towards. + /// The quad surrounding all selected objects. The center of this determines the position of the axis. + /// The position to flip. public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position) { var centre = quad.Centre; @@ -73,6 +76,9 @@ namespace osu.Game.Utils /// Given a flip axis vector, a surrounding quad for all selected objects, and a position, /// will return the flipped position in screen space coordinates. /// + /// The vector indicating the direction to flip towards. This is perpendicular to the mirroring axis. + /// The quad surrounding all selected objects. The center of this determines the position of the axis. + /// The position to flip. public static Vector2 GetFlippedPosition(Vector2 axis, Quad quad, Vector2 position) { var centre = quad.Centre; From ae47671d17e6322a688f24a090020f6188539c1e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 13 Aug 2024 14:21:42 +0200 Subject: [PATCH 057/308] clarify angle ranges in HandleFlip --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index da98da5238..bac0a5e273 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -138,15 +138,17 @@ namespace osu.Game.Rulesets.Osu.Edit switch (gridToolbox.GridType.Value) { case PositionSnapGridType.Square: - flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 405) % 90 - 45)); + flipAxis = GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 45) % 90 - 45)); flipAxis = direction == Direction.Vertical ? flipAxis.PerpendicularLeft : flipAxis; break; case PositionSnapGridType.Triangle: // Hex grid has 3 axes, so you can not directly flip over one of the axes, // however it's still possible to achieve that flip by combining multiple flips over the other axes. + // Angle degree range for vertical = (-120, -60] + // Angle degree range for horizontal = [-30, 30) flipAxis = direction == Direction.Vertical - ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 390) % 60 + 60)) + ? GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360 + 30) % 60 + 60)) : GeometryUtils.RotateVector(Vector2.UnitX, -((gridToolbox.GridLinesRotation.Value + 360) % 60 - 30)); break; } From 4cc38cea63a97611d6f5fbc902700c2e374d4dae Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 14:24:03 +0200 Subject: [PATCH 058/308] fix last anchor converting to implicit segment --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8a8964ccd4..b0173b3ae3 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -450,7 +450,7 @@ namespace osu.Game.Beatmaps.Formats // Explicit segments have a new format in which the type is injected into the middle of the control point string. // To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point. // One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments - bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE; + bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1; // Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable. // Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder. From a2e26ba9ffb93308e73263a7243833d429b873cb Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 16 Aug 2024 14:24:55 +0200 Subject: [PATCH 059/308] Fix perfect curve anchors losing type between reloads --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 8e6ffa20cc..d4e3053856 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -347,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Legacy vertices[i] = new PathControlPoint { Position = points[i] }; // Edge-case rules (to match stable). - if (type == PathType.PERFECT_CURVE) + if (type == PathType.PERFECT_CURVE && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { int endPointLength = endPoint == null ? 0 : 1; From 09ca190b8ddb44d27b16763f6a6c19d71332e001 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 00:10:15 +0200 Subject: [PATCH 060/308] re-implement ConvexHull 100% original --- osu.Game/Utils/GeometryUtils.cs | 39 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 810eda9950..d4c1dc2db7 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -154,33 +154,38 @@ namespace osu.Game.Utils { List p = points.ToList(); - if (p.Count <= 1) + if (p.Count < 3) return p; - int n = p.Count, k = 0; - List hull = new List(new Vector2[2 * n]); - p.Sort((a, b) => a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X)); - // Build lower hull - for (int i = 0; i < n; ++i) + List upper = new List(); + List lower = new List(); + + // Build the lower hull + for (int i = 0; i < p.Count; i++) { - while (k >= 2 && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) - k--; - hull[k] = p[i]; - k++; + while (lower.Count >= 2 && cross(lower[^2], lower[^1], p[i]) <= 0) + lower.RemoveAt(lower.Count - 1); + + lower.Add(p[i]); } - // Build upper hull - for (int i = n - 2, t = k + 1; i >= 0; i--) + // Build the upper hull + for (int i = p.Count - 1; i >= 0; i--) { - while (k >= t && cross(hull[k - 2], hull[k - 1], p[i]) <= 0) - k--; - hull[k] = p[i]; - k++; + while (upper.Count >= 2 && cross(upper[^2], upper[^1], p[i]) <= 0) + upper.RemoveAt(upper.Count - 1); + + upper.Add(p[i]); } - return hull.Take(k - 1).ToList(); + // Remove the last point of each half because it's a duplicate of the first point of the other half + lower.RemoveAt(lower.Count - 1); + upper.RemoveAt(upper.Count - 1); + + lower.AddRange(upper); + return lower; float cross(Vector2 o, Vector2 a, Vector2 b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); } From ff3bffc7d9e0e955afe3ad3ad498c2d3259e6877 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 00:30:57 +0200 Subject: [PATCH 061/308] add test --- osu.Game.Tests/Utils/GeometryUtilsTest.cs | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 osu.Game.Tests/Utils/GeometryUtilsTest.cs diff --git a/osu.Game.Tests/Utils/GeometryUtilsTest.cs b/osu.Game.Tests/Utils/GeometryUtilsTest.cs new file mode 100644 index 0000000000..ded4656ac1 --- /dev/null +++ b/osu.Game.Tests/Utils/GeometryUtilsTest.cs @@ -0,0 +1,33 @@ +// 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.Game.Utils; +using osuTK; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class GeometryUtilsTest + { + [TestCase(new int[] { }, new int[] { })] + [TestCase(new[] { 0, 0 }, new[] { 0, 0 })] + [TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })] + [TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, new[] { 0, 0, 1, 1, 2, 0, 1, -1 })] + [TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, new[] { 0, 0, 4, 10, 2, -1 })] + public void TestConvexHull(int[] values, int[] expected) + { + var points = new Vector2[values.Length / 2]; + for (int i = 0; i < values.Length; i += 2) + points[i / 2] = new Vector2(values[i], values[i + 1]); + + var expectedPoints = new Vector2[expected.Length / 2]; + for (int i = 0; i < expected.Length; i += 2) + expectedPoints[i / 2] = new Vector2(expected[i], expected[i + 1]); + + var hull = GeometryUtils.GetConvexHull(points); + + Assert.That(hull, Is.EquivalentTo(expectedPoints)); + } + } +} From db568bfb79d5d037e8a0e9d1485917421483da9a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 01:01:17 +0200 Subject: [PATCH 062/308] add tests --- .../Editor/TestSceneSliderChangeStates.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderChangeStates.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderChangeStates.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderChangeStates.cs new file mode 100644 index 0000000000..0b8f2f7417 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderChangeStates.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public partial class TestSceneSliderChangeStates : TestSceneOsuEditor + { + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [TestCase(SplineType.Catmull)] + [TestCase(SplineType.BSpline)] + [TestCase(SplineType.Linear)] + [TestCase(SplineType.PerfectCurve)] + public void TestSliderRetainsCurveTypes(SplineType splineType) + { + Slider? slider = null; + PathType pathType = new PathType(splineType); + + AddStep("add slider", () => EditorBeatmap.Add(slider = new Slider + { + StartTime = 500, + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, pathType), + new PathControlPoint(new Vector2(200, 0), pathType), + }) + })); + AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType)); + AddStep("remove object", () => EditorBeatmap.Remove(slider)); + AddAssert("slider removed", () => EditorBeatmap.HitObjects.Count == 0); + addUndoSteps(); + AddAssert("slider not removed", () => EditorBeatmap.HitObjects.Count == 1); + AddAssert("slider has correct spline type", () => ((Slider)EditorBeatmap.HitObjects[0]).Path.ControlPoints.All(p => p.Type == pathType)); + } + + private void addUndoSteps() => AddStep("undo", () => Editor.Undo()); + } +} From eefd7cf0833e3e18a40b698c5a090b7934ec1205 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Wed, 21 Aug 2024 12:03:15 +0200 Subject: [PATCH 063/308] add back protection against perfect curve segments with > 3 points --- .../Objects/Legacy/ConvertHitObjectParser.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index d4e3053856..c518a3e8b2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -347,17 +347,23 @@ namespace osu.Game.Rulesets.Objects.Legacy vertices[i] = new PathControlPoint { Position = points[i] }; // Edge-case rules (to match stable). - if (type == PathType.PERFECT_CURVE && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) + if (type == PathType.PERFECT_CURVE) { int endPointLength = endPoint == null ? 0 : 1; - if (vertices.Length + endPointLength != 3) - type = PathType.BEZIER; - else if (isLinear(points[0], points[1], endPoint ?? points[2])) + if (FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) { - // osu-stable special-cased colinear perfect curves to a linear path - type = PathType.LINEAR; + if (vertices.Length + endPointLength != 3) + type = PathType.BEZIER; + else if (isLinear(points[0], points[1], endPoint ?? points[2])) + { + // osu-stable special-cased colinear perfect curves to a linear path + type = PathType.LINEAR; + } } + else if (vertices.Length + endPointLength > 3) + // Lazer supports perfect curves with less than 3 points and colinear points + type = PathType.BEZIER; } // The first control point must have a definite type. From 6f1664f0a60fc08995d737e40272b61742fbe580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 16:30:49 +0900 Subject: [PATCH 064/308] Add beat-synced animation to break overlay I've been meaning to make the progress bar synchronise with the beat rather than a continuous countdown, just to give the overlay a bit more of a rhythmic feel. Not completely happy with how this feels but I think it's a start? I had to refactor how the break overlay works in the process. It no longer creates transforms for all breaks ahead-of-time, which could be argued as a better way of doing things. It's more dynamically able to handle breaks now (maybe useful for the future, who knows). --- .../Visual/Gameplay/TestSceneBreakTracker.cs | 13 ++- osu.Game/Screens/Play/BreakOverlay.cs | 104 ++++++++++-------- osu.Game/Screens/Play/BreakTracker.cs | 21 ++-- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Utils/PeriodTracker.cs | 24 +++- 5 files changed, 102 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs index ea21262fc0..21b6495865 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBreakTracker.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osuTK.Graphics; @@ -38,9 +40,10 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, }, breakTracker = new TestBreakTracker(), - breakOverlay = new BreakOverlay(true, null) + breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset())) { ProcessCustomClock = false, + BreakTracker = breakTracker, } }; } @@ -55,9 +58,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestShowBreaks() { - setClock(false); - - addShowBreakStep(2); addShowBreakStep(5); addShowBreakStep(15); } @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep($"show '{seconds}s' break", () => { - breakOverlay.Breaks = breakTracker.Breaks = new List + breakTracker.Breaks = new List { new BreakPeriod(Clock.CurrentTime, Clock.CurrentTime + seconds * 1000) }; @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadBreaksStep(string breakDescription, IReadOnlyList breaks) { - AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks); + AddStep($"load {breakDescription}", () => breakTracker.Breaks = breaks); seekAndAssertBreak("seek back to 0", 0, false); } @@ -182,6 +182,7 @@ namespace osu.Game.Tests.Visual.Gameplay } public TestBreakTracker() + : base(0, new ScoreProcessor(new OsuRuleset())) { FramedManualClock = new FramedClock(manualClock = new ManualClock()); ProcessCustomClock = false; diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 120d72a8e7..7fc3dba3eb 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -10,15 +10,18 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.Break; +using osu.Game.Utils; namespace osu.Game.Screens.Play { - public partial class BreakOverlay : Container + public partial class BreakOverlay : BeatSyncedContainer { /// /// The duration of the break overlay fading. @@ -26,26 +29,14 @@ namespace osu.Game.Screens.Play public const double BREAK_FADE_DURATION = BreakPeriod.MIN_BREAK_DURATION / 2; private const float remaining_time_container_max_size = 0.3f; - private const int vertical_margin = 25; + private const int vertical_margin = 15; private readonly Container fadeContainer; - private IReadOnlyList breaks = Array.Empty(); - - public IReadOnlyList Breaks - { - get => breaks; - set - { - breaks = value; - - if (IsLoaded) - initializeBreaks(); - } - } - public override bool RemoveCompletedTransforms => false; + public BreakTracker BreakTracker { get; init; } = null!; + private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; @@ -53,11 +44,15 @@ namespace osu.Game.Screens.Play private readonly ScoreProcessor scoreProcessor; private readonly BreakInfo info; + private readonly IBindable currentPeriod = new Bindable(); + public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) { this.scoreProcessor = scoreProcessor; RelativeSizeAxes = Axes.Both; + MinimumBeatLength = 200; + Child = fadeContainer = new Container { Alpha = 0, @@ -114,13 +109,13 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.Centre, Origin = Anchor.BottomCentre, - Margin = new MarginPadding { Bottom = vertical_margin }, + Y = -vertical_margin, }, info = new BreakInfo { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = vertical_margin }, + Y = vertical_margin, }, breakArrows = new BreakArrows { @@ -134,51 +129,68 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); - initializeBreaks(); info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy); ((IBindable)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank); + + currentPeriod.BindTo(BreakTracker.CurrentPeriod); + currentPeriod.BindValueChanged(updateDisplay, true); } + private float remainingTimeForCurrentPeriod => + currentPeriod.Value == null ? 0 : (float)Math.Max(0, (currentPeriod.Value.Value.End - Time.Current - BREAK_FADE_DURATION) / currentPeriod.Value.Value.Duration); + protected override void Update() { base.Update(); - remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + if (currentPeriod.Value != null) + { + remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + remainingTimeCounter.X = -(remainingTimeForCurrentPeriod - 0.5f) * 30; + info.X = (remainingTimeForCurrentPeriod - 0.5f) * 30; + } } - private void initializeBreaks() + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (currentPeriod.Value == null) + return; + + float timeBoxTargetWidth = (float)Math.Max(0, (remainingTimeForCurrentPeriod - timingPoint.BeatLength / currentPeriod.Value.Value.Duration)); + remainingTimeBox.ResizeWidthTo(timeBoxTargetWidth, timingPoint.BeatLength * 2, Easing.OutQuint); + } + + private void updateDisplay(ValueChangedEvent period) { FinishTransforms(true); Scheduler.CancelDelayedTasks(); - foreach (var b in breaks) + if (period.NewValue == null) + return; + + var b = period.NewValue.Value; + + using (BeginAbsoluteSequence(b.Start)) { - if (!b.HasEffect) - continue; + fadeContainer.FadeIn(BREAK_FADE_DURATION); + breakArrows.Show(BREAK_FADE_DURATION); - using (BeginAbsoluteSequence(b.StartTime)) + remainingTimeAdjustmentBox + .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) + .Delay(b.Duration - BREAK_FADE_DURATION) + .ResizeWidthTo(0); + + remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod); + + remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); + + using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) { - fadeContainer.FadeIn(BREAK_FADE_DURATION); - breakArrows.Show(BREAK_FADE_DURATION); - - remainingTimeAdjustmentBox - .ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint) - .Delay(b.Duration - BREAK_FADE_DURATION) - .ResizeWidthTo(0); - - remainingTimeBox - .ResizeWidthTo(0, b.Duration - BREAK_FADE_DURATION) - .Then() - .ResizeWidthTo(1); - - remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); - - using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) - { - fadeContainer.FadeOut(BREAK_FADE_DURATION); - breakArrows.Hide(BREAK_FADE_DURATION); - } + fadeContainer.FadeOut(BREAK_FADE_DURATION); + breakArrows.Hide(BREAK_FADE_DURATION); } } } diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index 20ef1dc4bf..3c3f31053a 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; @@ -18,7 +16,7 @@ namespace osu.Game.Screens.Play private readonly ScoreProcessor scoreProcessor; private readonly double gameplayStartTime; - private PeriodTracker breaks; + private PeriodTracker breaks = new PeriodTracker(Enumerable.Empty()); /// /// Whether the gameplay is currently in a break. @@ -27,6 +25,8 @@ namespace osu.Game.Screens.Play private readonly BindableBool isBreakTime = new BindableBool(true); + public readonly Bindable CurrentPeriod = new Bindable(); + public IReadOnlyList Breaks { set @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play } } - public BreakTracker(double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null) + public BreakTracker(double gameplayStartTime, ScoreProcessor scoreProcessor) { this.gameplayStartTime = gameplayStartTime; this.scoreProcessor = scoreProcessor; @@ -55,9 +55,16 @@ namespace osu.Game.Screens.Play { double time = Clock.CurrentTime; - isBreakTime.Value = breaks?.IsInAny(time) == true - || time < gameplayStartTime - || scoreProcessor?.HasCompleted.Value == true; + if (breaks.IsInAny(time, out var currentBreak)) + { + CurrentPeriod.Value = currentBreak; + isBreakTime.Value = true; + } + else + { + CurrentPeriod.Value = null; + isBreakTime.Value = time < gameplayStartTime || scoreProcessor.HasCompleted.Value; + } } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 91bd0a676b..2a66c3d5d3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -468,7 +468,7 @@ namespace osu.Game.Screens.Play { Clock = DrawableRuleset.FrameStableClock, ProcessCustomClock = false, - Breaks = working.Beatmap.Breaks + BreakTracker = breakTracker, }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), diff --git a/osu.Game/Utils/PeriodTracker.cs b/osu.Game/Utils/PeriodTracker.cs index ba77702247..2c62684ac4 100644 --- a/osu.Game/Utils/PeriodTracker.cs +++ b/osu.Game/Utils/PeriodTracker.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace osu.Game.Utils @@ -24,8 +25,17 @@ namespace osu.Game.Utils /// Whether the provided time is in any of the added periods. /// /// The time value to check. - public bool IsInAny(double time) + public bool IsInAny(double time) => IsInAny(time, out _); + + /// + /// Whether the provided time is in any of the added periods. + /// + /// The time value to check. + /// The period which matched. + public bool IsInAny(double time, [NotNullWhen(true)] out Period? period) { + period = null; + if (periods.Count == 0) return false; @@ -41,7 +51,15 @@ namespace osu.Game.Utils } var nearest = periods[nearestIndex]; - return time >= nearest.Start && time <= nearest.End; + bool isInAny = time >= nearest.Start && time <= nearest.End; + + if (isInAny) + { + period = nearest; + return true; + } + + return false; } } @@ -57,6 +75,8 @@ namespace osu.Game.Utils /// public readonly double End; + public double Duration => End - Start; + public Period(double start, double end) { if (start >= end) From 90d06d4496d727baf5da700906ad20ce7e2a66d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Aug 2024 16:37:27 +0900 Subject: [PATCH 065/308] Add slight parallax to centre content --- osu.Game/Screens/Play/BreakOverlay.cs | 58 +++++++++++++++------------ 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 7fc3dba3eb..fd2a3cc62f 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -89,33 +89,41 @@ namespace osu.Game.Screens.Play }, } }, - remainingTimeAdjustmentBox = new Container + new ParallaxContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Width = 0, - Child = remainingTimeBox = new Circle + RelativeSizeAxes = Axes.Both, + ParallaxAmount = -0.008f, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = 8, - Masking = true, - } - }, - remainingTimeCounter = new RemainingTimeCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Y = -vertical_margin, - }, - info = new BreakInfo - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Y = vertical_margin, + remainingTimeAdjustmentBox = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Width = 0, + Child = remainingTimeBox = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 8, + Masking = true, + } + }, + remainingTimeCounter = new RemainingTimeCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Y = -vertical_margin, + }, + info = new BreakInfo + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Y = vertical_margin, + }, + }, }, breakArrows = new BreakArrows { From 47a52d10ebb488b3d9e8feaee29d113a548bd916 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 15:32:59 +0900 Subject: [PATCH 066/308] Revert "Add slight parallax to centre content" This reverts commit 90d06d4496d727baf5da700906ad20ce7e2a66d9. --- osu.Game/Screens/Play/BreakOverlay.cs | 58 ++++++++++++--------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index fd2a3cc62f..7fc3dba3eb 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -89,41 +89,33 @@ namespace osu.Game.Screens.Play }, } }, - new ParallaxContainer + remainingTimeAdjustmentBox = new Container { - RelativeSizeAxes = Axes.Both, - ParallaxAmount = -0.008f, - Children = new Drawable[] + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Width = 0, + Child = remainingTimeBox = new Circle { - remainingTimeAdjustmentBox = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Width = 0, - Child = remainingTimeBox = new Circle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = 8, - Masking = true, - } - }, - remainingTimeCounter = new RemainingTimeCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Y = -vertical_margin, - }, - info = new BreakInfo - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Y = vertical_margin, - }, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 8, + Masking = true, + } + }, + remainingTimeCounter = new RemainingTimeCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Y = -vertical_margin, + }, + info = new BreakInfo + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Y = vertical_margin, }, breakArrows = new BreakArrows { From eb70a1b72d9d9476fb3a151a4af102fefd3d1593 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Aug 2024 15:57:42 +0900 Subject: [PATCH 067/308] Change middle text to only animate initially --- osu.Game/Screens/Play/BreakOverlay.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 7fc3dba3eb..7f9e879b44 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -144,12 +144,7 @@ namespace osu.Game.Screens.Play { base.Update(); - if (currentPeriod.Value != null) - { - remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); - remainingTimeCounter.X = -(remainingTimeForCurrentPeriod - 0.5f) * 30; - info.X = (remainingTimeForCurrentPeriod - 0.5f) * 30; - } + remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) @@ -187,6 +182,12 @@ namespace osu.Game.Screens.Play remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); + remainingTimeCounter.MoveToX(-50) + .MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint); + + info.MoveToX(50) + .MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint); + using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION)) { fadeContainer.FadeOut(BREAK_FADE_DURATION); From be0e2efda2b37a31abc5318303b418709856ceb1 Mon Sep 17 00:00:00 2001 From: Fabep Date: Wed, 28 Aug 2024 09:51:17 +0200 Subject: [PATCH 068/308] Removed on click event for expanding the Mod Customisation Header. --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index abd48a0dcb..57fe99ce4a 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -112,20 +112,6 @@ namespace osu.Game.Overlays.Mods }, true); } - protected override bool OnClick(ClickEvent e) - { - if (Enabled.Value) - { - ExpandedState.Value = ExpandedState.Value switch - { - ModCustomisationPanelState.Collapsed => ModCustomisationPanelState.Expanded, - _ => ModCustomisationPanelState.Collapsed - }; - } - - return base.OnClick(e); - } - private bool touchedThisFrame; protected override bool OnTouchDown(TouchDownEvent e) From 6adaf6a41faeeb09e9f53993c6ac2b91e9c9a0bf Mon Sep 17 00:00:00 2001 From: Fabep Date: Wed, 28 Aug 2024 10:09:47 +0200 Subject: [PATCH 069/308] Changed ModCustomisationPanelState names --- .../Visual/UserInterface/TestSceneModCustomisationPanel.cs | 6 +++--- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 2 +- osu.Game/Overlays/Mods/ModCustomisationPanel.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index 0d8ea05612..d93f62935a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -58,19 +58,19 @@ namespace osu.Game.Tests.Visual.UserInterface { SelectedMods.Value = new[] { new OsuModDoubleTime() }; panel.Enabled.Value = true; - panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod; }); AddStep("set DA", () => { SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }; panel.Enabled.Value = true; - panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod; }); AddStep("set FL+WU+DA+AD", () => { SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }; panel.Enabled.Value = true; - panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; + panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod; }); AddStep("set empty", () => { diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 57fe99ce4a..5ddcf01b88 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -130,7 +130,7 @@ namespace osu.Game.Overlays.Mods if (Enabled.Value) { if (!touchedThisFrame && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) - panel.ExpandedState.Value = ModCustomisationPanelState.ExpandedByHover; + panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; } return base.OnHover(e); diff --git a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs index 522481bc6b..03a1b3d0dd 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationPanel.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationPanel.cs @@ -227,7 +227,7 @@ namespace osu.Game.Overlays.Mods { base.Update(); - if (ExpandedState.Value == ModCustomisationPanelState.ExpandedByHover + if (ExpandedState.Value == ModCustomisationPanelState.Expanded && !ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position) && inputManager.DraggedDrawable == null) { @@ -239,8 +239,8 @@ namespace osu.Game.Overlays.Mods public enum ModCustomisationPanelState { Collapsed = 0, - ExpandedByHover = 1, - Expanded = 2, + Expanded = 1, + ExpandedByMod = 2, } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 74890df5d9..cdc0fbbd96 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -368,7 +368,7 @@ namespace osu.Game.Overlays.Mods customisationPanel.Enabled.Value = true; if (anyModPendingConfiguration) - customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded; + customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.ExpandedByMod; } else { From 636ee50eb9cbdc2d5d75de884b4be79ddd914e45 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 31 Aug 2024 23:03:10 +0900 Subject: [PATCH 070/308] Rename to VelopackUpdateManager --- osu.Desktop/OsuGameDesktop.cs | 2 +- .../{VeloUpdateManager.cs => VelopackUpdateManager.cs} | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) rename osu.Desktop/Updater/{VeloUpdateManager.cs => VelopackUpdateManager.cs} (96%) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index ee73c84ba3..94f6ef0fc3 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -100,7 +100,7 @@ namespace osu.Desktop if (!string.IsNullOrEmpty(packageManaged)) return new NoActionUpdateManager(); - return new VeloUpdateManager(); + return new VelopackUpdateManager(); } public override bool RestartAppWhenExited() diff --git a/osu.Desktop/Updater/VeloUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs similarity index 96% rename from osu.Desktop/Updater/VeloUpdateManager.cs rename to osu.Desktop/Updater/VelopackUpdateManager.cs index 6d3eb3f3f0..5cdc87e539 100644 --- a/osu.Desktop/Updater/VeloUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -11,11 +11,10 @@ using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; using Velopack; using Velopack.Sources; -using UpdateManager = Velopack.UpdateManager; namespace osu.Desktop.Updater { - public partial class VeloUpdateManager : Game.Updater.UpdateManager + public partial class VelopackUpdateManager : Game.Updater.UpdateManager { private readonly UpdateManager updateManager; private INotificationOverlay notificationOverlay = null!; @@ -26,7 +25,7 @@ namespace osu.Desktop.Updater [Resolved] private ILocalUserPlayInfo? localUserInfo { get; set; } - public VeloUpdateManager() + public VelopackUpdateManager() { const string? github_token = null; // TODO: populate. updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), new UpdateOptions From a038799c4745bf8f47189d13beec664f708785ac Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Sat, 31 Aug 2024 17:14:53 -0400 Subject: [PATCH 071/308] Update osu.Desktop.csproj bump version --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index ffa0c30b0c..3588317b8a 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -25,7 +25,7 @@ - + From b990af6adad67f239c31f3cc28ccfe657c9428d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 13:08:14 +0900 Subject: [PATCH 072/308] Use full name --- osu.Desktop/Program.cs | 4 ++-- osu.sln.DotSettings | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 92c8f2104c..609af2a8a7 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -31,7 +31,7 @@ namespace osu.Desktop public static void Main(string[] args) { // Velopack needs to run before anything else - setupVelo(); + setupVelopack(); if (OperatingSystem.IsWindows()) { @@ -164,7 +164,7 @@ namespace osu.Desktop return false; } - private static void setupVelo() + private static void setupVelopack() { VelopackApp .Build() diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index a792b956dd..38686d8508 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -1060,5 +1060,6 @@ private void load() True True True + True True True From 42e1168b35072115839e266ada4baebb9cfb6915 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 2 Sep 2024 01:23:05 -0400 Subject: [PATCH 073/308] Remove github token variable & pass null for the github token --- osu.Desktop/Updater/VelopackUpdateManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 5cdc87e539..eb1463483f 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -27,8 +27,7 @@ namespace osu.Desktop.Updater public VelopackUpdateManager() { - const string? github_token = null; // TODO: populate. - updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), new UpdateOptions + updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions { AllowVersionDowngrade = true }); From 38a62eed4458ea0897105537e5cfc28830798019 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 16:29:41 +0900 Subject: [PATCH 074/308] Add automatic downloading support when spectating a multiplayer room --- .../Match/MultiplayerSpectateButton.cs | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 1d308ed39c..8bc3704261 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -3,12 +3,19 @@ #nullable disable +using System.Linq; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -46,10 +53,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); operationInProgress.BindValueChanged(_ => updateState()); + automaticallyDownload = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); } protected override void OnRoomUpdated() @@ -77,6 +85,70 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match button.Enabled.Value = Client.Room != null && Client.Room.State != MultiplayerRoomState.Closed && !operationInProgress.Value; + + Scheduler.AddOnce(checkForAutomaticDownload); } + + #region Automatic download handling + + [Resolved] + private BeatmapLookupCache beatmapLookupCache { get; set; } + + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + private CancellationTokenSource downloadCheckCancellation; + + private Bindable automaticallyDownload; + + protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) + { + base.PlaylistItemChanged(item); + Scheduler.AddOnce(checkForAutomaticDownload); + } + + private void checkForAutomaticDownload() + { + MultiplayerPlaylistItem item = Client.Room?.Playlist.FirstOrDefault(i => !i.Expired); + + downloadCheckCancellation?.Cancel(); + + if (item == null) + return; + + if (!automaticallyDownload.Value) + return; + + // While we can support automatic downloads when not spectating, there are some usability concerns. + // - In host rotate mode, this could potentially be unwanted by some users (even though they want automatic downloads everywhere else). + // - When first joining a room, the expectation should be that the user is checking out the room, and they may not immediately want to download the selected beatmap. + // + // Rather than over-complicating this flow, let's only auto-download when spectating for the time being. + // A potential path forward would be to have a local auto-download checkbox above the playlist item list area. + if (Client.LocalUser?.State != MultiplayerUserState.Spectating) + return; + + // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. + // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. + beatmapLookupCache + .GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token) + .ContinueWith(resolved => Schedule(() => + { + var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; + + if (beatmapSet == null) + return; + + if (beatmaps.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID })) + return; + + beatmapDownloader.Download(beatmapSet); + })); + } + + #endregion } } From 6227e4f848ce0fc24c54cd81d47e9bb9268242d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 16:35:10 +0900 Subject: [PATCH 075/308] Apply NRT to `MultiplayerSpectateButton` --- .../Match/MultiplayerSpectateButton.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index 8bc3704261..bb6cd6cdaa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using System.Threading; using osu.Framework.Allocation; @@ -23,12 +21,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public partial class MultiplayerSpectateButton : MultiplayerRoomComposite { [Resolved] - private OngoingOperationTracker ongoingOperationTracker { get; set; } + private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!; [Resolved] - private OsuColour colours { get; set; } + private OsuColour colours { get; set; } = null!; - private IBindable operationInProgress; + private IBindable operationInProgress = null!; private readonly RoundedButton button; @@ -92,17 +90,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match #region Automatic download handling [Resolved] - private BeatmapLookupCache beatmapLookupCache { get; set; } + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; [Resolved] private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; - private CancellationTokenSource downloadCheckCancellation; + private Bindable automaticallyDownload = null!; - private Bindable automaticallyDownload; + private CancellationTokenSource? downloadCheckCancellation; protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) { @@ -112,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void checkForAutomaticDownload() { - MultiplayerPlaylistItem item = Client.Room?.Playlist.FirstOrDefault(i => !i.Expired); + MultiplayerPlaylistItem? item = Client.Room?.Playlist.FirstOrDefault(i => !i.Expired); downloadCheckCancellation?.Cancel(); From f8a6a6a8aef5db35a3d5cdf7aca50247d4cfedd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 16:43:46 +0900 Subject: [PATCH 076/308] Request restart asynchronously to avoid blocking update thread --- osu.Desktop/OsuGameDesktop.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 94f6ef0fc3..c75a3f0a1a 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Reflection; using System.Runtime.Versioning; +using System.Threading.Tasks; using Microsoft.Win32; using osu.Desktop.Performance; using osu.Desktop.Security; @@ -18,6 +19,7 @@ using osu.Desktop.Windows; using osu.Framework.Allocation; using osu.Game.IO; using osu.Game.IPC; +using osu.Game.Online.Multiplayer; using osu.Game.Performance; using osu.Game.Utils; @@ -105,16 +107,8 @@ namespace osu.Desktop public override bool RestartAppWhenExited() { - try - { - Velopack.UpdateExe.Start(); - return true; - } - catch (Exception e) - { - Logger.Error(e, "Failed to restart application"); - return base.RestartAppWhenExited(); - } + Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget(); + return true; } protected override void LoadComplete() From 68e6fa286e00c0fef777d5d2fcfa49f503a5bd45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 16:46:29 +0900 Subject: [PATCH 077/308] Make comment about velopack's init a bit more loud --- osu.Desktop/Program.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 609af2a8a7..5103663815 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -30,7 +30,9 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { - // Velopack needs to run before anything else + // IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else. + // This has bitten us in the rear before (bricked updater), and although the underlying issue from + // last time has been fixed, let's not tempt fate. setupVelopack(); if (OperatingSystem.IsWindows()) From cd9b82253e4639d3a94dc049244c9ccbcc59ea47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 17:20:33 +0900 Subject: [PATCH 078/308] Pass through correct update to apply when calling `WaitExitThenApplyUpdates` --- osu.Desktop/Updater/VelopackUpdateManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index eb1463483f..4d2535ed32 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -52,7 +52,7 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; - var info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + UpdateInfo? info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); // Handle no updates available. if (info == null) @@ -65,7 +65,7 @@ namespace osu.Desktop.Updater { Activated = () => { - restartToApplyUpdate(); + restartToApplyUpdate(null); return true; } }); @@ -78,7 +78,7 @@ namespace osu.Desktop.Updater { notification = new UpdateProgressNotification { - CompletionClickAction = restartToApplyUpdate, + CompletionClickAction = () => restartToApplyUpdate(info), }; Schedule(() => notificationOverlay.Post(notification)); @@ -117,9 +117,9 @@ namespace osu.Desktop.Updater return true; } - private bool restartToApplyUpdate() + private bool restartToApplyUpdate(UpdateInfo? info) { - updateManager.WaitExitThenApplyUpdates(null); + updateManager.WaitExitThenApplyUpdates(info?.TargetFullRelease); Schedule(() => game.AttemptExit()); return true; } From 171ac0f5104efea07c262cffa1622d6d758ed289 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 17:26:14 +0900 Subject: [PATCH 079/308] Fix incorrect osu!catch snap display when last object is a juice stream Addresses https://github.com/ppy/osu/discussions/29678. --- osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs index c3103bd204..6f5b32a41d 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs @@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Catch.Edit // The implementation below is probably correct but should be checked if/when exposed via controls. float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime()); - float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX); + + float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX; + float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX); return actualDistance / expectedDistance; } From 10901075bef07ba3abba1a22c0166a5bcd23c6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 11:27:44 +0200 Subject: [PATCH 080/308] Modify existing test coverage to demonstrate failure with touch --- .../TestSceneModCustomisationPanel.cs | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs index d93f62935a..04cb129630 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModCustomisationPanel.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; @@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestExpandedStatePersistsWhenClicked() + public void TestHoverExpandsAndCollapsesWhenHeaderTouched() { AddStep("add customisable mod", () => { @@ -128,34 +129,20 @@ namespace osu.Game.Tests.Visual.UserInterface panel.Enabled.Value = true; }); - AddStep("hover header", () => InputManager.MoveMouseTo(header)); - checkExpanded(true); - - AddStep("click", () => InputManager.Click(MouseButton.Left)); - checkExpanded(false); - AddStep("click", () => InputManager.Click(MouseButton.Left)); - checkExpanded(true); - - AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One)); - checkExpanded(true); - - AddStep("click", () => InputManager.Click(MouseButton.Left)); - checkExpanded(false); - } - - [Test] - public void TestHoverExpandsAndCollapsesWhenHeaderClicked() - { - AddStep("add customisable mod", () => + AddStep("touch header", () => { - SelectedMods.Value = new[] { new OsuModDoubleTime() }; - panel.Enabled.Value = true; + var touch = new Touch(TouchSource.Touch1, header.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + Schedule(() => InputManager.EndTouch(touch)); }); - - AddStep("hover header", () => InputManager.MoveMouseTo(header)); checkExpanded(true); - AddStep("click", () => InputManager.Click(MouseButton.Left)); + AddStep("touch away from header", () => + { + var touch = new Touch(TouchSource.Touch1, header.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)); + InputManager.BeginTouch(touch); + Schedule(() => InputManager.EndTouch(touch)); + }); checkExpanded(false); } From 5211e606b5b0c9919a2675c09a263ec88acb8c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 11:33:43 +0200 Subject: [PATCH 081/308] Fix mod customisation header being non-functional on mobile As expected the previous touch handling would prevent the header from working entirely when click handling was removed from the header. --- .../Overlays/Mods/ModCustomisationHeader.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 5ddcf01b88..32fd5a37aa 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -112,26 +112,10 @@ namespace osu.Game.Overlays.Mods }, true); } - private bool touchedThisFrame; - - protected override bool OnTouchDown(TouchDownEvent e) - { - if (Enabled.Value) - { - touchedThisFrame = true; - Schedule(() => touchedThisFrame = false); - } - - return base.OnTouchDown(e); - } - protected override bool OnHover(HoverEvent e) { - if (Enabled.Value) - { - if (!touchedThisFrame && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) - panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; - } + if (Enabled.Value && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) + panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; return base.OnHover(e); } From 872d14ed88b8daf04121cd1a5483bbf8cb5dbd79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 19:18:43 +0900 Subject: [PATCH 082/308] Fix incorrect handling of ordered playlist items --- .../Match/MultiplayerSpectateButton.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index bb6cd6cdaa..fa26a85786 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -58,6 +57,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match automaticallyDownload = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); } + protected override void LoadComplete() + { + base.LoadComplete(); + + CurrentPlaylistItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true); + } + protected override void OnRoomUpdated() { base.OnRoomUpdated(); @@ -102,19 +108,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private CancellationTokenSource? downloadCheckCancellation; - protected override void PlaylistItemChanged(MultiplayerPlaylistItem item) - { - base.PlaylistItemChanged(item); - Scheduler.AddOnce(checkForAutomaticDownload); - } - private void checkForAutomaticDownload() { - MultiplayerPlaylistItem? item = Client.Room?.Playlist.FirstOrDefault(i => !i.Expired); + PlaylistItem? currentItem = CurrentPlaylistItem.Value; downloadCheckCancellation?.Cancel(); - if (item == null) + if (currentItem == null) return; if (!automaticallyDownload.Value) @@ -132,7 +132,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache - .GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token) + .GetBeatmapAsync(currentItem.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token) .ContinueWith(resolved => Schedule(() => { var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; From 7b139433772804c5c5f044abc57589e9cf638af5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Sep 2024 19:20:05 +0900 Subject: [PATCH 083/308] Handle changes to the automatic download setting immediately --- .../OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs index fa26a85786..ea7ab2dce3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs @@ -54,7 +54,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); operationInProgress.BindValueChanged(_ => updateState()); + automaticallyDownload = config.GetBindable(OsuSetting.AutomaticallyDownloadMissingBeatmaps); + automaticallyDownload.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload)); } protected override void LoadComplete() From 16c2c140377286c2a90430d23a37e5c7da43cc34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 2 Sep 2024 14:45:29 +0200 Subject: [PATCH 084/308] Adjust tests further to match new UX --- .../TestSceneModSelectOverlay.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index f21c64f7fe..280497e861 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -241,12 +241,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); assertCustomisationToggleState(disabled: false, active: true); - AddStep("dismiss mod customisation via toggle", () => - { - InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - assertCustomisationToggleState(disabled: false, active: false); + AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); + assertCustomisationToggleState(disabled: false, active: true); AddStep("reset mods", () => SelectedMods.SetDefault()); AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick()); @@ -664,7 +660,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value), () => Is.EqualTo(1)); - AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType().Single().TriggerClick()); + AddStep("open customisation area", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single())); assertCustomisationToggleState(disabled: false, active: true); AddStep("hover over mod settings slider", () => @@ -976,7 +972,7 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); - AddStep("open customisation panel", () => this.ChildrenOfType().Single().TriggerClick()); + AddStep("open customisation panel", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); AddAssert("search lost focus", () => !this.ChildrenOfType().Single().HasFocus); AddStep("press tab", () => InputManager.Key(Key.Tab)); @@ -991,15 +987,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); AddAssert("mods not deselected", () => SelectedMods.Value.Single() is OsuModDoubleTime); - AddStep("move mouse to scroll bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().Single().ScreenSpaceDrawQuad.BottomLeft + new Vector2(10f, -5f))); + AddStep("move mouse to customisation panel", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().First())); AddStep("scroll down", () => InputManager.ScrollVerticalBy(-10f)); AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType().Single().IsScrolledToStart()); - AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left)); - AddAssert("search still not focused", () => !this.ChildrenOfType().Single().HasFocus); - AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("customisation panel closed by click", + AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("customisation panel closed", () => this.ChildrenOfType().Single().ExpandedState.Value, () => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed)); From e3457d850157808928b7ef912b8ecc562826f5a2 Mon Sep 17 00:00:00 2001 From: Fabep Date: Mon, 2 Sep 2024 19:14:08 +0200 Subject: [PATCH 085/308] Mod customisation header's color is now based on the state of the panel rather than the hover of the container. --- .../Overlays/Mods/ModCustomisationHeader.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 32fd5a37aa..d8191f5ba5 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -20,7 +19,7 @@ using static osu.Game.Overlays.Mods.ModCustomisationPanel; namespace osu.Game.Overlays.Mods { - public partial class ModCustomisationHeader : OsuHoverContainer + public partial class ModCustomisationHeader : OsuClickableContainer { private Box background = null!; private Box backgroundFlash = null!; @@ -29,8 +28,6 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - protected override IEnumerable EffectTargets => new[] { background }; - public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); private readonly ModCustomisationPanel panel; @@ -52,6 +49,7 @@ namespace osu.Game.Overlays.Mods background = new Box { RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark3, }, backgroundFlash = new Box { @@ -84,9 +82,6 @@ namespace osu.Game.Overlays.Mods } } }; - - IdleColour = colourProvider.Dark3; - HoverColour = colourProvider.Light4; } protected override void LoadComplete() @@ -110,6 +105,20 @@ namespace osu.Game.Overlays.Mods { icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); }, true); + + panel.ExpandedState.BindValueChanged(v => + { + switch (v.NewValue) + { + case ModCustomisationPanelState.Expanded: + case ModCustomisationPanelState.ExpandedByMod: + fadeBackgroundColor(colourProvider.Light4); + break; + default: + fadeBackgroundColor(colourProvider.Dark3); + break; + }; + }, false); } protected override bool OnHover(HoverEvent e) @@ -119,5 +128,10 @@ namespace osu.Game.Overlays.Mods return base.OnHover(e); } + + private void fadeBackgroundColor(Color4 color) + { + background.FadeColour(color, 500, Easing.OutQuint); + } } } From 582ffcfc9722342f73d83778fc950cacc1bb9439 Mon Sep 17 00:00:00 2001 From: Fabep Date: Mon, 2 Sep 2024 19:17:07 +0200 Subject: [PATCH 086/308] Woopsie! I accidentally added one too many semi-colons, so I moved it here into the commit instead ; --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index d8191f5ba5..da77d8dac8 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.Mods default: fadeBackgroundColor(colourProvider.Dark3); break; - }; + } }, false); } From a2b15fcdee7feccabe605e06984c0b851843de31 Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Tue, 3 Sep 2024 00:59:42 -0400 Subject: [PATCH 087/308] rework code logic to make more sense analysis container creates settings and the settings items are created more nicely also removed use of localized strings because that can be done separately --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 4 +- .../UI/OsuAnalysisContainer.cs | 35 +++++++----- .../UI/OsuAnalysisSettings.cs | 53 ++++--------------- .../PlayerSettingsOverlayStrings.cs | 20 ------- osu.Game/Rulesets/Ruleset.cs | 4 +- osu.Game/Rulesets/UI/AnalysisContainer.cs | 13 ++++- .../Play/PlayerSettings/AnalysisSettings.cs | 13 ++--- osu.Game/Screens/Play/ReplayPlayer.cs | 8 +-- 8 files changed, 55 insertions(+), 95 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 8beeeac34e..a8a1d98bf3 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Overlays.Settings; +using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; @@ -37,7 +38,6 @@ using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; -using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; @@ -361,7 +361,7 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } - public override AnalysisSettings CreateAnalysisSettings(DrawableRuleset drawableRuleset) => new OsuAnalysisSettings(drawableRuleset); + public override OsuAnalysisContainer CreateAnalysisContainer(Replay replay, Playfield playfield) => new OsuAnalysisContainer(replay, playfield); public override bool EditorShowScrollSpeed => false; } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 7d8ae6980c..3bddc479ef 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -1,10 +1,10 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. + +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; @@ -21,27 +21,33 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisContainer : AnalysisContainer { - public Bindable HitMarkerEnabled = new BindableBool(); - public Bindable AimMarkersEnabled = new BindableBool(); - public Bindable AimLinesEnabled = new BindableBool(); + public new OsuAnalysisSettings AnalysisSettings => (OsuAnalysisSettings)base.AnalysisSettings; + + protected new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; protected HitMarkersContainer HitMarkers; protected AimMarkersContainer AimMarkers; protected AimLinesContainer AimLines; - public OsuAnalysisContainer(Replay replay) - : base(replay) + public OsuAnalysisContainer(Replay replay, Playfield playfield) + : base(replay, playfield) { InternalChildren = new Drawable[] { + AimLines = new AimLinesContainer { Depth = float.MaxValue }, HitMarkers = new HitMarkersContainer(), - AimMarkers = new AimMarkersContainer { Depth = float.MinValue }, - AimLines = new AimLinesContainer { Depth = float.MaxValue } + AimMarkers = new AimMarkersContainer { Depth = float.MinValue } }; + } - HitMarkerEnabled.ValueChanged += e => HitMarkers.FadeTo(e.NewValue ? 1 : 0); - AimMarkersEnabled.ValueChanged += e => AimMarkers.FadeTo(e.NewValue ? 1 : 0); - AimLinesEnabled.ValueChanged += e => AimLines.FadeTo(e.NewValue ? 1 : 0); + protected override OsuAnalysisSettings CreateAnalysisSettings() + { + var settings = new OsuAnalysisSettings(); + settings.HitMarkersEnabled.ValueChanged += e => HitMarkers.FadeTo(e.NewValue ? 1 : 0); + settings.AimMarkersEnabled.ValueChanged += e => AimMarkers.FadeTo(e.NewValue ? 1 : 0); + settings.AimLinesEnabled.ValueChanged += e => AimLines.FadeTo(e.NewValue ? 1 : 0); + settings.CursorHideEnabled.ValueChanged += e => Playfield.Cursor.FadeTo(e.NewValue ? 0 : 1); + return settings; } [BackgroundDependencyLoader] @@ -51,6 +57,11 @@ namespace osu.Game.Rulesets.Osu.UI AimMarkers.Hide(); AimLines.Hide(); + LoadReplay(); + } + + protected void LoadReplay() + { bool leftHeld = false; bool rightHeld = false; diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index b3d5231ade..c45b893a1c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -2,58 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Localisation; -using osu.Game.Replays; -using osu.Game.Rulesets.UI; +using osu.Game.Configuration; using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisSettings : AnalysisSettings { - protected new DrawableOsuRuleset DrawableRuleset => (DrawableOsuRuleset)base.DrawableRuleset; + [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool HitMarkersEnabled { get; } = new BindableBool(); - private readonly PlayerCheckbox hitMarkerToggle; - private readonly PlayerCheckbox aimMarkerToggle; - private readonly PlayerCheckbox aimLinesToggle; + [SettingSource("Aim markers", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool AimMarkersEnabled { get; } = new BindableBool(); - public OsuAnalysisSettings(DrawableRuleset drawableRuleset) - : base(drawableRuleset) - { - PlayerCheckbox hideCursorToggle; + [SettingSource("Aim lines", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool AimLinesEnabled { get; } = new BindableBool(); - Children = new Drawable[] - { - hitMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HitMarkers }, - aimMarkerToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimMarkers }, - aimLinesToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.AimLines }, - hideCursorToggle = new PlayerCheckbox { LabelText = PlayerSettingsOverlayStrings.HideCursor } - }; - - hideCursorToggle.Current.BindValueChanged(onCursorToggle); - } - - private void onCursorToggle(ValueChangedEvent hide) - { - // this only hides half the cursor - if (hide.NewValue) - { - DrawableRuleset.Playfield.Cursor.FadeOut(); - } - else - { - DrawableRuleset.Playfield.Cursor.FadeIn(); - } - } - - public override AnalysisContainer CreateAnalysisContainer(Replay replay) - { - var analysisContainer = new OsuAnalysisContainer(replay); - analysisContainer.HitMarkerEnabled.BindTo(hitMarkerToggle.Current); - analysisContainer.AimMarkersEnabled.BindTo(aimMarkerToggle.Current); - analysisContainer.AimLinesEnabled.BindTo(aimLinesToggle.Current); - return analysisContainer; - } + [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool CursorHideEnabled { get; } = new BindableBool(); } } diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs index 017cc9bf82..60874da561 100644 --- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs +++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs @@ -19,26 +19,6 @@ namespace osu.Game.Localisation /// public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame"); - /// - /// "Hit markers" - /// - public static LocalisableString HitMarkers => new TranslatableString(getKey(@"hit_markers"), @"Hit markers"); - - /// - /// "Aim markers" - /// - public static LocalisableString AimMarkers => new TranslatableString(getKey(@"aim_markers"), @"Aim markers"); - - /// - /// "Hide cursor" - /// - public static LocalisableString HideCursor => new TranslatableString(getKey(@"hide_cursor"), @"Hide cursor"); - - /// - /// "Aim lines" - /// - public static LocalisableString AimLines => new TranslatableString(getKey(@"aim_lines"), @"Aim lines"); - /// /// "Seek backward {0} seconds" /// diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 4626698190..fdf43c2f09 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Overlays.Settings; +using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; @@ -27,7 +28,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Edit.Setup; -using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; using osu.Game.Users; @@ -410,6 +410,6 @@ namespace osu.Game.Rulesets public virtual DifficultySection? CreateEditorDifficultySection() => null; - public virtual AnalysisSettings? CreateAnalysisSettings(DrawableRuleset drawableRuleset) => null; + public virtual AnalysisContainer? CreateAnalysisContainer(Replay replay, Playfield playfield) => null; } } diff --git a/osu.Game/Rulesets/UI/AnalysisContainer.cs b/osu.Game/Rulesets/UI/AnalysisContainer.cs index 62d54374e7..69a71cf06e 100644 --- a/osu.Game/Rulesets/UI/AnalysisContainer.cs +++ b/osu.Game/Rulesets/UI/AnalysisContainer.cs @@ -3,16 +3,25 @@ using osu.Framework.Graphics.Containers; using osu.Game.Replays; +using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.UI { - public partial class AnalysisContainer : Container + public abstract partial class AnalysisContainer : Container { protected Replay Replay; + protected Playfield Playfield; - public AnalysisContainer(Replay replay) + public AnalysisSettings AnalysisSettings; + + public AnalysisContainer(Replay replay, Playfield playfield) { Replay = replay; + Playfield = playfield; + + AnalysisSettings = CreateAnalysisSettings(); } + + protected abstract AnalysisSettings CreateAnalysisSettings(); } } diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index 91531b28b4..e1f77cef12 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -1,21 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Replays; -using osu.Game.Rulesets.UI; +using osu.Game.Configuration; namespace osu.Game.Screens.Play.PlayerSettings { - public abstract partial class AnalysisSettings : PlayerSettingsGroup + public partial class AnalysisSettings : PlayerSettingsGroup { - protected DrawableRuleset DrawableRuleset; - - protected AnalysisSettings(DrawableRuleset drawableRuleset) + public AnalysisSettings() : base("Analysis Settings") { - DrawableRuleset = drawableRuleset; + AddRange(this.CreateSettingsControls()); } - - public abstract AnalysisContainer CreateAnalysisContainer(Replay replay); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 5d604003c8..af9568c08c 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -72,12 +72,12 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - var analysisSettings = DrawableRuleset.Ruleset.CreateAnalysisSettings(DrawableRuleset); + var analysisContainer = DrawableRuleset.Ruleset.CreateAnalysisContainer(GameplayState.Score.Replay, DrawableRuleset.Playfield); - if (analysisSettings != null) + if (analysisContainer != null) { - HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisSettings); - DrawableRuleset.Playfield.AddAnalysisContainer(analysisSettings.CreateAnalysisContainer(GameplayState.Score.Replay)); + HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisContainer.AnalysisSettings); + DrawableRuleset.Playfield.AddAnalysisContainer(analysisContainer); } } From 56db29d0f5c7d798144660186ededd38a8ff03ae Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Tue, 3 Sep 2024 01:38:54 -0400 Subject: [PATCH 088/308] make test go indefinitely --- .../TestSceneHitMarker.cs | 91 ------------ .../TestSceneOsuAnalysisContainer.cs | 134 ++++++++++++++++++ .../UI/OsuAnalysisContainer.cs | 2 + 3 files changed, 136 insertions(+), 91 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs deleted file mode 100644 index e4c48f96b8..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitMarker.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable disable - -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using osu.Game.Replays; -using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Replays; -using osu.Game.Tests.Visual; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Tests -{ - public partial class TestSceneHitMarker : OsuTestScene - { - private TestOsuAnalysisContainer analysisContainer; - - [Test] - public void TestHitMarkers() - { - createAnalysisContainer(); - AddStep("enable hit markers", () => analysisContainer.HitMarkerEnabled.Value = true); - AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); - AddStep("disable hit markers", () => analysisContainer.HitMarkerEnabled.Value = false); - AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); - } - - [Test] - public void TestAimMarker() - { - createAnalysisContainer(); - AddStep("enable aim markers", () => analysisContainer.AimMarkersEnabled.Value = true); - AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); - AddStep("disable aim markers", () => analysisContainer.AimMarkersEnabled.Value = false); - AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); - } - - [Test] - public void TestAimLines() - { - createAnalysisContainer(); - AddStep("enable aim lines", () => analysisContainer.AimLinesEnabled.Value = true); - AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); - AddStep("disable aim lines", () => analysisContainer.AimLinesEnabled.Value = false); - AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); - } - - private void createAnalysisContainer() - { - AddStep("create new analysis container", () => Child = analysisContainer = new TestOsuAnalysisContainer(fabricateReplay())); - } - - private Replay fabricateReplay() - { - var frames = new List(); - - for (int i = 0; i < 50; i++) - { - frames.Add(new OsuReplayFrame - { - Time = Time.Current + i * 15, - Position = new Vector2(20 + i * 10, 20), - Actions = - { - i % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton - } - }); - } - - return new Replay { Frames = frames }; - } - - private partial class TestOsuAnalysisContainer : OsuAnalysisContainer - { - public TestOsuAnalysisContainer(Replay replay) - : base(replay) - { - } - - public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); - - public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); - - public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs new file mode 100644 index 0000000000..a173256557 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -0,0 +1,134 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Threading; +using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Replays; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public partial class TestSceneOsuAnalysisContainer : OsuTestScene + { + private TestOsuAnalysisContainer analysisContainer; + + [BackgroundDependencyLoader] + private void load() + { + Child = analysisContainer = createAnalysisContainer(); + } + + [Test] + public void TestHitMarkers() + { + var loop = createAnalysisContainer(); + AddStep("enable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = true); + AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); + AddStep("disable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = false); + AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); + } + + [Test] + public void TestAimMarker() + { + var loop = createAnalysisContainer(); + AddStep("enable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = true); + AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); + AddStep("disable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = false); + AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); + } + + [Test] + public void TestAimLines() + { + var loop = createAnalysisContainer(); + AddStep("enable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = true); + AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); + AddStep("disable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = false); + AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); + } + + private TestOsuAnalysisContainer createAnalysisContainer() => new TestOsuAnalysisContainer(); + + private partial class TestOsuAnalysisContainer : OsuAnalysisContainer + { + public TestOsuAnalysisContainer() + : base(new Replay(), new OsuPlayfield()) + { + } + + [BackgroundDependencyLoader] + private void load() + { + Replay = fabricateReplay(); + LoadReplay(); + + makeReplayLoop(); + } + + private void makeReplayLoop() + { + Scheduler.AddDelayed(() => + { + Replay = fabricateReplay(); + + HitMarkers.Clear(); + AimMarkers.Clear(); + AimLines.Clear(); + + LoadReplay(); + + makeReplayLoop(); + }, 15000); + } + + public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); + + public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); + + public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; + + private Replay fabricateReplay() + { + var frames = new List(); + var random = new Random(); + int posX = 250; + int posY = 250; + bool leftOrRight = false; + + for (int i = 0; i < 1000; i++) + { + posX = Math.Clamp(posX + random.Next(-10, 11), 0, 500); + posY = Math.Clamp(posY + random.Next(-10, 11), 0, 500); + + var actions = new List(); + + if (i % 20 == 0) + { + actions.Add(leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton); + leftOrRight = !leftOrRight; + } + + frames.Add(new OsuReplayFrame + { + Time = Time.Current + i * 15, + Position = new Vector2(posX, posY), + Actions = actions + }); + } + + return new Replay { Frames = frames }; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 3bddc479ef..2f341a35bb 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -142,6 +142,8 @@ namespace osu.Game.Rulesets.Osu.UI public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); + public void Clear() => lifetimeManager.ClearEntries(); + private void entryBecameAlive(LifetimeEntry entry) { aliveEntries.Add((AimPointEntry)entry); From a549cdd5b9f1b8a968e9a3e5d57c926db22b8675 Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Tue, 3 Sep 2024 04:49:50 -0400 Subject: [PATCH 089/308] persist analysis settings --- .../UI/OsuAnalysisContainer.cs | 27 ++++++++++++------- .../UI/OsuAnalysisSettings.cs | 10 +++++++ osu.Game/Configuration/OsuConfigManager.cs | 10 +++++++ 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 2f341a35bb..4eff147772 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -1,5 +1,4 @@ - -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -38,28 +37,38 @@ namespace osu.Game.Rulesets.Osu.UI HitMarkers = new HitMarkersContainer(), AimMarkers = new AimMarkersContainer { Depth = float.MinValue } }; + } protected override OsuAnalysisSettings CreateAnalysisSettings() { var settings = new OsuAnalysisSettings(); - settings.HitMarkersEnabled.ValueChanged += e => HitMarkers.FadeTo(e.NewValue ? 1 : 0); - settings.AimMarkersEnabled.ValueChanged += e => AimMarkers.FadeTo(e.NewValue ? 1 : 0); - settings.AimLinesEnabled.ValueChanged += e => AimLines.FadeTo(e.NewValue ? 1 : 0); - settings.CursorHideEnabled.ValueChanged += e => Playfield.Cursor.FadeTo(e.NewValue ? 0 : 1); + settings.HitMarkersEnabled.ValueChanged += e => toggleHitMarkers(e.NewValue); + settings.AimMarkersEnabled.ValueChanged += e => toggleAimMarkers(e.NewValue); + settings.AimLinesEnabled.ValueChanged += e => toggleAimLines(e.NewValue); + settings.CursorHideEnabled.ValueChanged += e => toggleCursorHidden(e.NewValue); return settings; } [BackgroundDependencyLoader] private void load() { - HitMarkers.Hide(); - AimMarkers.Hide(); - AimLines.Hide(); + toggleHitMarkers(AnalysisSettings.HitMarkersEnabled.Value); + toggleAimMarkers(AnalysisSettings.AimMarkersEnabled.Value); + toggleAimLines(AnalysisSettings.AimLinesEnabled.Value); + toggleCursorHidden(AnalysisSettings.CursorHideEnabled.Value); LoadReplay(); } + private void toggleHitMarkers(bool value) => HitMarkers.FadeTo(value ? 1 : 0); + + private void toggleAimMarkers(bool value) => AimMarkers.FadeTo(value ? 1 : 0); + + private void toggleAimLines(bool value) => AimLines.FadeTo(value ? 1 : 0); + + private void toggleCursorHidden(bool value) => Playfield.Cursor.FadeTo(value ? 0 : 1); + protected void LoadReplay() { bool leftHeld = false; diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index c45b893a1c..ae81b2c0b8 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Screens.Play.PlayerSettings; @@ -20,5 +21,14 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool CursorHideEnabled { get; } = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); + config.BindWith(OsuSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); + config.BindWith(OsuSetting.ReplayAimLinesEnabled, AimLinesEnabled); + config.BindWith(OsuSetting.ReplayCursorHideEnabled, CursorHideEnabled); + } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 8d6c244b35..8b75c9c934 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -154,6 +154,12 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.IncreaseFirstObjectVisibility, true); SetDefault(OsuSetting.GameplayDisableWinKey, true); + // Replay + SetDefault(OsuSetting.ReplayHitMarkersEnabled, false); + SetDefault(OsuSetting.ReplayAimMarkersEnabled, false); + SetDefault(OsuSetting.ReplayAimLinesEnabled, false); + SetDefault(OsuSetting.ReplayCursorHideEnabled, false); + // Update SetDefault(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -413,6 +419,10 @@ namespace osu.Game.Configuration EditorShowHitMarkers, EditorAutoSeekOnPlacement, DiscordRichPresence, + ReplayHitMarkersEnabled, + ReplayAimMarkersEnabled, + ReplayAimLinesEnabled, + ReplayCursorHideEnabled, ShowOnlineExplicitContent, LastProcessedMetadataId, From 6c89c4eed6f2e619b2932324c9173d0dcaf9f39d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Sep 2024 18:50:57 +0900 Subject: [PATCH 090/308] Fix rewind causing weirdness with progress bar animation --- osu.Game/Screens/Play/BreakOverlay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 7f9e879b44..4ed8b69a77 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -145,6 +145,13 @@ namespace osu.Game.Screens.Play base.Update(); remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth); + + // Keep things simple by resetting beat synced transforms on a rewind. + if (Clock.ElapsedFrameTime < 0) + { + remainingTimeBox.ClearTransforms(targetMember: nameof(Width)); + remainingTimeBox.Width = remainingTimeForCurrentPeriod; + } } protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) From 08224b416e9664f264366a4280edd891d1439782 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Sep 2024 19:11:34 +0900 Subject: [PATCH 091/308] Simplify update process by caching pending update info and early-handling edge case --- osu.Desktop/Updater/VelopackUpdateManager.cs | 28 +++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 4d2535ed32..bf7122d720 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -25,11 +25,13 @@ namespace osu.Desktop.Updater [Resolved] private ILocalUserPlayInfo? localUserInfo { get; set; } + private UpdateInfo? pendingUpdate; + public VelopackUpdateManager() { updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions { - AllowVersionDowngrade = true + AllowVersionDowngrade = true, }); } @@ -52,33 +54,33 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; - UpdateInfo? info = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); - - // Handle no updates available. - if (info == null) + if (pendingUpdate != null) { - // If there's no updates pending restart, bail and retry later. - if (!updateManager.IsUpdatePendingRestart) return false; - // If there is an update pending restart, show the notification to restart again. notificationOverlay.Post(new UpdateApplicationCompleteNotification { Activated = () => { - restartToApplyUpdate(null); + restartToApplyUpdate(); return true; } }); return true; } + pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); + + // Handle no updates available. + if (pendingUpdate == null) + return false; + scheduleRecheck = false; if (notification == null) { notification = new UpdateProgressNotification { - CompletionClickAction = () => restartToApplyUpdate(info), + CompletionClickAction = restartToApplyUpdate, }; Schedule(() => notificationOverlay.Post(notification)); @@ -88,7 +90,7 @@ namespace osu.Desktop.Updater try { - await updateManager.DownloadUpdatesAsync(info, p => notification.Progress = p / 100f).ConfigureAwait(false); + await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false); notification.State = ProgressNotificationState.Completed; } @@ -117,9 +119,9 @@ namespace osu.Desktop.Updater return true; } - private bool restartToApplyUpdate(UpdateInfo? info) + private bool restartToApplyUpdate() { - updateManager.WaitExitThenApplyUpdates(info?.TargetFullRelease); + updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease); Schedule(() => game.AttemptExit()); return true; } From b61023385a85a4508cd28b153d94fa233cc251a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Sep 2024 19:16:10 +0900 Subject: [PATCH 092/308] Don't log probable network failures to sentry --- osu.Desktop/Updater/VelopackUpdateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index bf7122d720..5b4d281f80 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -105,7 +105,7 @@ namespace osu.Desktop.Updater { // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion. scheduleRecheck = true; - Logger.Error(e, @"update check failed!"); + Logger.Log($@"update check failed ({e.Message})"); } finally { From 1f122ab38de24e819697fba9c1eb2c3b6b8f4ac1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Sep 2024 23:57:18 +0900 Subject: [PATCH 093/308] Apply new rider migration --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index a792b956dd..2d0b7446f3 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -853,6 +853,7 @@ See the LICENCE file in the repository root for full licence text. True True True + True True True True From 421f245c3102bb2ab3719b7ff4717b401a0db11a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 14:31:59 +0900 Subject: [PATCH 094/308] Fix per-frame allocations in `BeatmapCarousel` --- .../SongSelect/TestSceneBeatmapCarousel.cs | 4 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 11 +++++++---- .../Carousel/DrawableCarouselBeatmapSet.cs | 18 ++++++++---------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index ec072a3dd2..218a67e818 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -1405,9 +1405,9 @@ namespace osu.Game.Tests.Visual.SongSelect yield return item; - if (item is DrawableCarouselBeatmapSet set) + if (item is DrawableCarouselBeatmapSet set && set.Beatmaps?.IsLoaded == true) { - foreach (var difficulty in set.DrawableBeatmaps) + foreach (var difficulty in set.Beatmaps) yield return difficulty; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 87cea45e87..f7e0eae4a5 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -857,8 +857,9 @@ namespace osu.Game.Screens.Select // Add those items within the previously found index range that should be displayed. foreach (var item in toDisplay) { - var panel = setPool.Get(p => p.Item = item); + var panel = setPool.Get(); + panel.Item = item; panel.Y = item.CarouselYPosition; Scroll.Add(panel); @@ -898,10 +899,12 @@ namespace osu.Game.Screens.Select Scroll.ChangeChildDepth(item, hasPassedSelection ? -item.Item.CarouselYPosition : item.Item.CarouselYPosition); } - if (item is DrawableCarouselBeatmapSet set) + if (item is DrawableCarouselBeatmapSet set && set.Beatmaps?.IsLoaded == true) { - foreach (var diff in set.DrawableBeatmaps) + foreach (var diff in set.Beatmaps) + { updateItem(diff, item); + } } } } @@ -1101,7 +1104,7 @@ namespace osu.Game.Screens.Select } /// - /// Update a item's x position and multiplicative alpha based on its y position and + /// Update an item's x position and multiplicative alpha based on its y position and /// the current scroll position. /// /// The item to be updated. diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 1cd8b065fc..9a01b46216 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -51,9 +51,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IBindable ruleset { get; set; } = null!; - public IEnumerable DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty() : beatmapContainer.AliveChildren; - - private Container? beatmapContainer; + public Container? Beatmaps; private BeatmapSetInfo beatmapSet = null!; @@ -126,7 +124,7 @@ namespace osu.Game.Screens.Select.Carousel Content.Clear(); Header.Clear(); - beatmapContainer = null; + Beatmaps = null; beatmapsLoadTask = null; if (Item == null) @@ -164,7 +162,7 @@ namespace osu.Game.Screens.Select.Carousel // if we are already displaying all the correct beatmaps, only run animation updates. // note that the displayed beatmaps may change due to the applied filter. // a future optimisation could add/remove only changed difficulties rather than reinitialise. - if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b))) + if (Beatmaps != null && visibleBeatmaps.Length == Beatmaps.Count && visibleBeatmaps.All(b => Beatmaps.Any(c => c.Item == b))) { updateBeatmapYPositions(); } @@ -173,17 +171,17 @@ namespace osu.Game.Screens.Select.Carousel // on selection we show our child beatmaps. // for now this is a simple drawable construction each selection. // can be improved in the future. - beatmapContainer = new Container + Beatmaps = new Container { X = 100, RelativeSizeAxes = Axes.Both, ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()!) }; - beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded => + beatmapsLoadTask = LoadComponentAsync(Beatmaps, loaded => { // make sure the pooled target hasn't changed. - if (beatmapContainer != loaded) + if (Beatmaps != loaded) return; Content.Child = loaded; @@ -244,7 +242,7 @@ namespace osu.Game.Screens.Select.Carousel private void updateBeatmapYPositions() { - if (beatmapContainer == null) + if (Beatmaps == null) return; if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted) @@ -254,7 +252,7 @@ namespace osu.Game.Screens.Select.Carousel bool isSelected = Item?.State.Value == CarouselItemState.Selected; - foreach (var panel in beatmapContainer) + foreach (var panel in Beatmaps) { Debug.Assert(panel.Item != null); From 97a51af5a06351d64944951963d82dba4df65c16 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 14:52:52 +0900 Subject: [PATCH 095/308] Fix one more unnecessary enumerator allocation --- .../Overlays/NotificationOverlayToastTray.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlayToastTray.cs b/osu.Game/Overlays/NotificationOverlayToastTray.cs index d2899f29b8..df07b4f138 100644 --- a/osu.Game/Overlays/NotificationOverlayToastTray.cs +++ b/osu.Game/Overlays/NotificationOverlayToastTray.cs @@ -153,8 +153,22 @@ namespace osu.Game.Overlays { base.Update(); - float height = toastFlow.Count > 0 ? toastFlow.DrawHeight + 120 : 0; - float alpha = toastFlow.Count > 0 ? MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * toastFlow.Children.Max(n => n.Alpha) : 0; + float height = 0; + float alpha = 0; + + if (toastFlow.Count > 0) + { + float maxNotificationAlpha = 0; + + foreach (var t in toastFlow) + { + if (t.Alpha > maxNotificationAlpha) + maxNotificationAlpha = t.Alpha; + } + + height = toastFlow.DrawHeight + 120; + alpha = MathHelper.Clamp(toastFlow.DrawHeight / 41, 0, 1) * maxNotificationAlpha; + } toastContentBackground.Height = (float)Interpolation.DampContinuously(toastContentBackground.Height, height, 10, Clock.ElapsedFrameTime); toastContentBackground.Alpha = (float)Interpolation.DampContinuously(toastContentBackground.Alpha, alpha, 10, Clock.ElapsedFrameTime); From dfe11dc68a4f381a3d6ec78d30462929dd3efe5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 15:25:36 +0900 Subject: [PATCH 096/308] Use `for` with exposed `IReadOnlyList` rather than making internal container public --- .../SongSelect/TestSceneBeatmapCarousel.cs | 4 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 +++----- .../Carousel/DrawableCarouselBeatmapSet.cs | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 218a67e818..ec072a3dd2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -1405,9 +1405,9 @@ namespace osu.Game.Tests.Visual.SongSelect yield return item; - if (item is DrawableCarouselBeatmapSet set && set.Beatmaps?.IsLoaded == true) + if (item is DrawableCarouselBeatmapSet set) { - foreach (var difficulty in set.Beatmaps) + foreach (var difficulty in set.DrawableBeatmaps) yield return difficulty; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f7e0eae4a5..a6a6a2f585 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -899,12 +899,10 @@ namespace osu.Game.Screens.Select Scroll.ChangeChildDepth(item, hasPassedSelection ? -item.Item.CarouselYPosition : item.Item.CarouselYPosition); } - if (item is DrawableCarouselBeatmapSet set && set.Beatmaps?.IsLoaded == true) + if (item is DrawableCarouselBeatmapSet set) { - foreach (var diff in set.Beatmaps) - { - updateItem(diff, item); - } + for (int i = 0; i < set.DrawableBeatmaps.Count; i++) + updateItem(set.DrawableBeatmaps[i], item); } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 9a01b46216..eba40994e2 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -51,7 +51,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private IBindable ruleset { get; set; } = null!; - public Container? Beatmaps; + public IReadOnlyList DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Array.Empty() : beatmapContainer; + + private Container? beatmapContainer; private BeatmapSetInfo beatmapSet = null!; @@ -124,7 +126,7 @@ namespace osu.Game.Screens.Select.Carousel Content.Clear(); Header.Clear(); - Beatmaps = null; + beatmapContainer = null; beatmapsLoadTask = null; if (Item == null) @@ -162,7 +164,7 @@ namespace osu.Game.Screens.Select.Carousel // if we are already displaying all the correct beatmaps, only run animation updates. // note that the displayed beatmaps may change due to the applied filter. // a future optimisation could add/remove only changed difficulties rather than reinitialise. - if (Beatmaps != null && visibleBeatmaps.Length == Beatmaps.Count && visibleBeatmaps.All(b => Beatmaps.Any(c => c.Item == b))) + if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b))) { updateBeatmapYPositions(); } @@ -171,17 +173,17 @@ namespace osu.Game.Screens.Select.Carousel // on selection we show our child beatmaps. // for now this is a simple drawable construction each selection. // can be improved in the future. - Beatmaps = new Container + beatmapContainer = new Container { X = 100, RelativeSizeAxes = Axes.Both, ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()!) }; - beatmapsLoadTask = LoadComponentAsync(Beatmaps, loaded => + beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded => { // make sure the pooled target hasn't changed. - if (Beatmaps != loaded) + if (beatmapContainer != loaded) return; Content.Child = loaded; @@ -242,7 +244,7 @@ namespace osu.Game.Screens.Select.Carousel private void updateBeatmapYPositions() { - if (Beatmaps == null) + if (beatmapContainer == null) return; if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted) @@ -252,7 +254,7 @@ namespace osu.Game.Screens.Select.Carousel bool isSelected = Item?.State.Value == CarouselItemState.Selected; - foreach (var panel in Beatmaps) + foreach (var panel in beatmapContainer) { Debug.Assert(panel.Item != null); From e564e8c04895336559a16b093cbc905de8162b9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 16:08:18 +0900 Subject: [PATCH 097/308] Add todo about fixing stutter on update application --- osu.Desktop/Updater/VelopackUpdateManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 5b4d281f80..527892413a 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -121,6 +121,8 @@ namespace osu.Desktop.Updater private bool restartToApplyUpdate() { + // TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665). + // Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart. updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease); Schedule(() => game.AttemptExit()); return true; From c89597b060cae084899db82c3b6c5040a17f794a Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Wed, 4 Sep 2024 03:37:52 -0400 Subject: [PATCH 098/308] fix config mistake --- .../Configuration/OsuRulesetConfigManager.cs | 11 +++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 3 --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 ++ .../UI/OsuAnalysisContainer.cs | 13 ++++++------- osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs | 17 ++++++++++++----- osu.Game/Configuration/OsuConfigManager.cs | 10 ---------- osu.Game/Rulesets/Ruleset.cs | 3 --- osu.Game/Rulesets/UI/AnalysisContainer.cs | 10 +++++----- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 ++ .../Play/PlayerSettings/AnalysisSettings.cs | 7 ++++++- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- 11 files changed, 45 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index 2056a50eda..23b7b9c1fa 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.ShowCursorTrail, true); SetDefault(OsuRulesetSetting.ShowCursorRipples, false); SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); + + SetDefault(OsuRulesetSetting.ReplayHitMarkersEnabled, false); + SetDefault(OsuRulesetSetting.ReplayAimMarkersEnabled, false); + SetDefault(OsuRulesetSetting.ReplayAimLinesEnabled, false); + SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false); } } @@ -34,5 +39,11 @@ namespace osu.Game.Rulesets.Osu.Configuration ShowCursorTrail, ShowCursorRipples, PlayfieldBorderStyle, + + // Replay + ReplayHitMarkersEnabled, + ReplayAimMarkersEnabled, + ReplayAimLinesEnabled, + ReplayCursorHideEnabled, } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index a8a1d98bf3..be48ef9acc 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -13,7 +13,6 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Overlays.Settings; -using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; @@ -361,8 +360,6 @@ namespace osu.Game.Rulesets.Osu return adjustedDifficulty; } - public override OsuAnalysisContainer CreateAnalysisContainer(Replay replay, Playfield playfield) => new OsuAnalysisContainer(replay, playfield); - public override bool EditorShowScrollSpeed => false; } } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index f0390ad716..3c6456957b 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -68,5 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI return 0; } } + + public override AnalysisContainer CreateAnalysisContainer(Replay replay) => new OsuAnalysisContainer(replay, this); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 4eff147772..57401edece 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -22,14 +22,14 @@ namespace osu.Game.Rulesets.Osu.UI { public new OsuAnalysisSettings AnalysisSettings => (OsuAnalysisSettings)base.AnalysisSettings; - protected new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; + protected new DrawableOsuRuleset DrawableRuleset => (DrawableOsuRuleset)base.DrawableRuleset; protected HitMarkersContainer HitMarkers; protected AimMarkersContainer AimMarkers; protected AimLinesContainer AimLines; - public OsuAnalysisContainer(Replay replay, Playfield playfield) - : base(replay, playfield) + public OsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) + : base(replay, drawableRuleset) { InternalChildren = new Drawable[] { @@ -37,12 +37,11 @@ namespace osu.Game.Rulesets.Osu.UI HitMarkers = new HitMarkersContainer(), AimMarkers = new AimMarkersContainer { Depth = float.MinValue } }; - } - protected override OsuAnalysisSettings CreateAnalysisSettings() + protected override OsuAnalysisSettings CreateAnalysisSettings(Ruleset ruleset) { - var settings = new OsuAnalysisSettings(); + var settings = new OsuAnalysisSettings((OsuRuleset)ruleset); settings.HitMarkersEnabled.ValueChanged += e => toggleHitMarkers(e.NewValue); settings.AimMarkersEnabled.ValueChanged += e => toggleAimMarkers(e.NewValue); settings.AimLinesEnabled.ValueChanged += e => toggleAimLines(e.NewValue); @@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI private void toggleAimLines(bool value) => AimLines.FadeTo(value ? 1 : 0); - private void toggleCursorHidden(bool value) => Playfield.Cursor.FadeTo(value ? 0 : 1); + private void toggleCursorHidden(bool value) => DrawableRuleset.Playfield.Cursor.FadeTo(value ? 0 : 1); protected void LoadReplay() { diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index ae81b2c0b8..6e11c87c3a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -4,12 +4,18 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { public partial class OsuAnalysisSettings : AnalysisSettings { + public OsuAnalysisSettings(Ruleset ruleset) + : base(ruleset) + { + } + [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HitMarkersEnabled { get; } = new BindableBool(); @@ -23,12 +29,13 @@ namespace osu.Game.Rulesets.Osu.UI public BindableBool CursorHideEnabled { get; } = new BindableBool(); [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(IRulesetConfigCache cache) { - config.BindWith(OsuSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); - config.BindWith(OsuSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); - config.BindWith(OsuSetting.ReplayAimLinesEnabled, AimLinesEnabled); - config.BindWith(OsuSetting.ReplayCursorHideEnabled, CursorHideEnabled); + var config = (OsuRulesetConfigManager)cache.GetConfigFor(Ruleset)!; + config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); + config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); + config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, AimLinesEnabled); + config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, CursorHideEnabled); } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 8b75c9c934..8d6c244b35 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -154,12 +154,6 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.IncreaseFirstObjectVisibility, true); SetDefault(OsuSetting.GameplayDisableWinKey, true); - // Replay - SetDefault(OsuSetting.ReplayHitMarkersEnabled, false); - SetDefault(OsuSetting.ReplayAimMarkersEnabled, false); - SetDefault(OsuSetting.ReplayAimLinesEnabled, false); - SetDefault(OsuSetting.ReplayCursorHideEnabled, false); - // Update SetDefault(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -419,10 +413,6 @@ namespace osu.Game.Configuration EditorShowHitMarkers, EditorAutoSeekOnPlacement, DiscordRichPresence, - ReplayHitMarkersEnabled, - ReplayAimMarkersEnabled, - ReplayAimLinesEnabled, - ReplayCursorHideEnabled, ShowOnlineExplicitContent, LastProcessedMetadataId, diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index fdf43c2f09..2e48b8e16f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -17,7 +17,6 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Overlays.Settings; -using osu.Game.Replays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; @@ -409,7 +408,5 @@ namespace osu.Game.Rulesets public virtual bool EditorShowScrollSpeed => true; public virtual DifficultySection? CreateEditorDifficultySection() => null; - - public virtual AnalysisContainer? CreateAnalysisContainer(Replay replay, Playfield playfield) => null; } } diff --git a/osu.Game/Rulesets/UI/AnalysisContainer.cs b/osu.Game/Rulesets/UI/AnalysisContainer.cs index 69a71cf06e..b6c2a8c1c8 100644 --- a/osu.Game/Rulesets/UI/AnalysisContainer.cs +++ b/osu.Game/Rulesets/UI/AnalysisContainer.cs @@ -10,18 +10,18 @@ namespace osu.Game.Rulesets.UI public abstract partial class AnalysisContainer : Container { protected Replay Replay; - protected Playfield Playfield; + protected DrawableRuleset DrawableRuleset; public AnalysisSettings AnalysisSettings; - public AnalysisContainer(Replay replay, Playfield playfield) + protected AnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) { Replay = replay; - Playfield = playfield; + DrawableRuleset = drawableRuleset; - AnalysisSettings = CreateAnalysisSettings(); + AnalysisSettings = CreateAnalysisSettings(drawableRuleset.Ruleset); } - protected abstract AnalysisSettings CreateAnalysisSettings(); + protected abstract AnalysisSettings CreateAnalysisSettings(Ruleset ruleset); } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index a28b2716cb..5b1f59d549 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -596,6 +596,8 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); + + public virtual AnalysisContainer CreateAnalysisContainer(Replay replay) => null; } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs index e1f77cef12..4c64eef92f 100644 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs @@ -2,14 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Configuration; +using osu.Game.Rulesets; namespace osu.Game.Screens.Play.PlayerSettings { public partial class AnalysisSettings : PlayerSettingsGroup { - public AnalysisSettings() + protected Ruleset Ruleset; + + public AnalysisSettings(Ruleset ruleset) : base("Analysis Settings") { + Ruleset = ruleset; + AddRange(this.CreateSettingsControls()); } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index af9568c08c..82a2f09250 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - var analysisContainer = DrawableRuleset.Ruleset.CreateAnalysisContainer(GameplayState.Score.Replay, DrawableRuleset.Playfield); + var analysisContainer = DrawableRuleset.CreateAnalysisContainer(GameplayState.Score.Replay); if (analysisContainer != null) { From 59ff8c498417f97760b83ccf55fdfb65fd454428 Mon Sep 17 00:00:00 2001 From: Sheppsu Date: Wed, 4 Sep 2024 03:38:13 -0400 Subject: [PATCH 099/308] fix analysis container creation --- .../TestSceneOsuAnalysisContainer.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index a173256557..0fc91513e6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -8,11 +8,12 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Threading; using osu.Game.Replays; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.UI; using osu.Game.Tests.Visual; using osuTK; @@ -31,7 +32,6 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestHitMarkers() { - var loop = createAnalysisContainer(); AddStep("enable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = true); AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); AddStep("disable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = false); @@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestAimMarker() { - var loop = createAnalysisContainer(); AddStep("enable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = true); AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); AddStep("disable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = false); @@ -51,19 +50,28 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestAimLines() { - var loop = createAnalysisContainer(); AddStep("enable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = true); AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); AddStep("disable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = false); AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); } - private TestOsuAnalysisContainer createAnalysisContainer() => new TestOsuAnalysisContainer(); + private TestOsuAnalysisContainer createAnalysisContainer() + { + var replay = new Replay(); + var ruleset = new OsuRuleset(); + var beatmap = new OsuBeatmap(); + var drawableRuleset = new DrawableOsuRuleset(ruleset, beatmap); + // Load playfield cursor to avoid errors + Add(drawableRuleset); + + return new TestOsuAnalysisContainer(replay, drawableRuleset); + } private partial class TestOsuAnalysisContainer : OsuAnalysisContainer { - public TestOsuAnalysisContainer() - : base(new Replay(), new OsuPlayfield()) + public TestOsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) + : base(replay, drawableRuleset) { } From 86309f4b4605b7ac82853006acf08bec8cb2a84f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 17:25:36 +0900 Subject: [PATCH 100/308] Revert "Woopsie! I accidentally added one too many semi-colons, so I moved it here into the commit instead ;" This reverts commit 582ffcfc9722342f73d83778fc950cacc1bb9439. Revert "Mod customisation header's color is now based on the state of the panel rather than the hover of the container." This reverts commit e3457d850157808928b7ef912b8ecc562826f5a2. --- .../Overlays/Mods/ModCustomisationHeader.cs | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index da77d8dac8..32fd5a37aa 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -19,7 +20,7 @@ using static osu.Game.Overlays.Mods.ModCustomisationPanel; namespace osu.Game.Overlays.Mods { - public partial class ModCustomisationHeader : OsuClickableContainer + public partial class ModCustomisationHeader : OsuHoverContainer { private Box background = null!; private Box backgroundFlash = null!; @@ -28,6 +29,8 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + protected override IEnumerable EffectTargets => new[] { background }; + public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); private readonly ModCustomisationPanel panel; @@ -49,7 +52,6 @@ namespace osu.Game.Overlays.Mods background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Dark3, }, backgroundFlash = new Box { @@ -82,6 +84,9 @@ namespace osu.Game.Overlays.Mods } } }; + + IdleColour = colourProvider.Dark3; + HoverColour = colourProvider.Light4; } protected override void LoadComplete() @@ -105,20 +110,6 @@ namespace osu.Game.Overlays.Mods { icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); }, true); - - panel.ExpandedState.BindValueChanged(v => - { - switch (v.NewValue) - { - case ModCustomisationPanelState.Expanded: - case ModCustomisationPanelState.ExpandedByMod: - fadeBackgroundColor(colourProvider.Light4); - break; - default: - fadeBackgroundColor(colourProvider.Dark3); - break; - } - }, false); } protected override bool OnHover(HoverEvent e) @@ -128,10 +119,5 @@ namespace osu.Game.Overlays.Mods return base.OnHover(e); } - - private void fadeBackgroundColor(Color4 color) - { - background.FadeColour(color, 500, Easing.OutQuint); - } } } From 7cd24ba58ecedd2971e4f06b637e1dab4bdeb00d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:00:07 +0900 Subject: [PATCH 101/308] Disallow mistimed firing of beat sync for break overlay for now It doesn't work well with pause/resume. --- osu.Game/Screens/Play/BreakOverlay.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs index 4ed8b69a77..1fdb9402bc 100644 --- a/osu.Game/Screens/Play/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -53,6 +53,10 @@ namespace osu.Game.Screens.Play MinimumBeatLength = 200; + // Doesn't play well with pause/unpause. + // This might mean that some beats don't animate if the user is running <60fps, but we'll deal with that if anyone notices. + AllowMistimedEventFiring = false; + Child = fadeContainer = new Container { Alpha = 0, From 045096b08ac771c649809546a53290d01d46857b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:00:48 +0900 Subject: [PATCH 102/308] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 2609fd42c3..5f3dd2f6f4 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 1056f4b441..9d9b42a163 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From cb9d1d49a225cbf58c253ea87ae600f9cb0716c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:01:06 +0900 Subject: [PATCH 103/308] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6952de2fa5..b2bf8c7eb9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From a417fec2347a01bb4e2ab1be308c474f4a240ab3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:35:27 +0900 Subject: [PATCH 104/308] Move analysis container implementation completely local to osu! ruleset --- .../TestSceneOsuAnalysisContainer.cs | 129 +++++++----------- .../UI/DrawableOsuRuleset.cs | 10 +- .../UI/OsuAnalysisContainer.cs | 51 +++---- .../UI/OsuAnalysisSettings.cs | 17 ++- osu.Game/Rulesets/Ruleset.cs | 2 - osu.Game/Rulesets/UI/AnalysisContainer.cs | 27 ---- osu.Game/Rulesets/UI/DrawableRuleset.cs | 2 - osu.Game/Rulesets/UI/Playfield.cs | 6 - .../Play/PlayerSettings/AnalysisSettings.cs | 21 --- osu.Game/Screens/Play/ReplayPlayer.cs | 8 -- 10 files changed, 93 insertions(+), 180 deletions(-) delete mode 100644 osu.Game/Rulesets/UI/AnalysisContainer.cs delete mode 100644 osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 0fc91513e6..536de8b41a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -1,13 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Replays; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Replays; @@ -21,51 +20,80 @@ namespace osu.Game.Rulesets.Osu.Tests { public partial class TestSceneOsuAnalysisContainer : OsuTestScene { - private TestOsuAnalysisContainer analysisContainer; + private TestOsuAnalysisContainer analysisContainer = null!; - [BackgroundDependencyLoader] - private void load() + [SetUpSteps] + public void SetUpSteps() { - Child = analysisContainer = createAnalysisContainer(); + AddStep("create analysis container", () => + { + DrawableOsuRuleset drawableRuleset = new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()); + + Children = new Drawable[] + { + drawableRuleset, + analysisContainer = new TestOsuAnalysisContainer(fabricateReplay(), drawableRuleset), + }; + }); } [Test] public void TestHitMarkers() { - AddStep("enable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = true); + AddStep("enable hit markers", () => analysisContainer.Settings.HitMarkersEnabled.Value = true); AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); - AddStep("disable hit markers", () => analysisContainer.AnalysisSettings.HitMarkersEnabled.Value = false); + AddStep("disable hit markers", () => analysisContainer.Settings.HitMarkersEnabled.Value = false); AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); } [Test] public void TestAimMarker() { - AddStep("enable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = true); + AddStep("enable aim markers", () => analysisContainer.Settings.AimMarkersEnabled.Value = true); AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); - AddStep("disable aim markers", () => analysisContainer.AnalysisSettings.AimMarkersEnabled.Value = false); + AddStep("disable aim markers", () => analysisContainer.Settings.AimMarkersEnabled.Value = false); AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); } [Test] public void TestAimLines() { - AddStep("enable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = true); + AddStep("enable aim lines", () => analysisContainer.Settings.AimLinesEnabled.Value = true); AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); - AddStep("disable aim lines", () => analysisContainer.AnalysisSettings.AimLinesEnabled.Value = false); + AddStep("disable aim lines", () => analysisContainer.Settings.AimLinesEnabled.Value = false); AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); } - private TestOsuAnalysisContainer createAnalysisContainer() + private Replay fabricateReplay() { - var replay = new Replay(); - var ruleset = new OsuRuleset(); - var beatmap = new OsuBeatmap(); - var drawableRuleset = new DrawableOsuRuleset(ruleset, beatmap); - // Load playfield cursor to avoid errors - Add(drawableRuleset); + var frames = new List(); + var random = new Random(); + int posX = 250; + int posY = 250; + bool leftOrRight = false; - return new TestOsuAnalysisContainer(replay, drawableRuleset); + for (int i = 0; i < 1000; i++) + { + posX = Math.Clamp(posX + random.Next(-10, 11), 0, 500); + posY = Math.Clamp(posY + random.Next(-10, 11), 0, 500); + + var actions = new List(); + + if (i % 20 == 0) + { + actions.Add(leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton); + leftOrRight = !leftOrRight; + } + + frames.Add(new OsuReplayFrame + { + Time = Time.Current + i * 15, + Position = new Vector2(posX, posY), + Actions = actions + }); + } + + return new Replay { Frames = frames }; } private partial class TestOsuAnalysisContainer : OsuAnalysisContainer @@ -75,68 +103,9 @@ namespace osu.Game.Rulesets.Osu.Tests { } - [BackgroundDependencyLoader] - private void load() - { - Replay = fabricateReplay(); - LoadReplay(); - - makeReplayLoop(); - } - - private void makeReplayLoop() - { - Scheduler.AddDelayed(() => - { - Replay = fabricateReplay(); - - HitMarkers.Clear(); - AimMarkers.Clear(); - AimLines.Clear(); - - LoadReplay(); - - makeReplayLoop(); - }, 15000); - } - public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); - public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); - public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; - - private Replay fabricateReplay() - { - var frames = new List(); - var random = new Random(); - int posX = 250; - int posY = 250; - bool leftOrRight = false; - - for (int i = 0; i < 1000; i++) - { - posX = Math.Clamp(posX + random.Next(-10, 11), 0, 500); - posY = Math.Clamp(posY + random.Next(-10, 11), 0, 500); - - var actions = new List(); - - if (i % 20 == 0) - { - actions.Add(leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton); - leftOrRight = !leftOrRight; - } - - frames.Add(new OsuReplayFrame - { - Time = Time.Current + i * 15, - Position = new Vector2(posX, posY), - Actions = actions - }); - } - - return new Replay { Frames = frames }; - } } } } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 3c6456957b..ba0768db5d 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -36,6 +36,14 @@ namespace osu.Game.Rulesets.Osu.UI { } + protected override void LoadComplete() + { + if (HasReplayLoaded.Value) + LoadComponentAsync(new OsuAnalysisContainer(ReplayScore.Replay, this), PlayfieldAdjustmentContainer.Add); + + base.LoadComplete(); + } + public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) => null; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor @@ -68,7 +76,5 @@ namespace osu.Game.Rulesets.Osu.UI return 0; } } - - public override AnalysisContainer CreateAnalysisContainer(Replay replay) => new OsuAnalysisContainer(replay, this); } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs index 57401edece..19e53944c9 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; @@ -18,62 +19,62 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI { - public partial class OsuAnalysisContainer : AnalysisContainer + public partial class OsuAnalysisContainer : CompositeDrawable { - public new OsuAnalysisSettings AnalysisSettings => (OsuAnalysisSettings)base.AnalysisSettings; + protected readonly HitMarkersContainer HitMarkers; + protected readonly AimMarkersContainer AimMarkers; + protected readonly AimLinesContainer AimLines; - protected new DrawableOsuRuleset DrawableRuleset => (DrawableOsuRuleset)base.DrawableRuleset; + public OsuAnalysisSettings Settings = null!; - protected HitMarkersContainer HitMarkers; - protected AimMarkersContainer AimMarkers; - protected AimLinesContainer AimLines; + private readonly Replay replay; + private readonly DrawableRuleset drawableRuleset; public OsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) - : base(replay, drawableRuleset) { + this.replay = replay; + this.drawableRuleset = drawableRuleset; + InternalChildren = new Drawable[] { - AimLines = new AimLinesContainer { Depth = float.MaxValue }, HitMarkers = new HitMarkersContainer(), - AimMarkers = new AimMarkersContainer { Depth = float.MinValue } + AimLines = new AimLinesContainer(), + AimMarkers = new AimMarkersContainer(), }; } - protected override OsuAnalysisSettings CreateAnalysisSettings(Ruleset ruleset) - { - var settings = new OsuAnalysisSettings((OsuRuleset)ruleset); - settings.HitMarkersEnabled.ValueChanged += e => toggleHitMarkers(e.NewValue); - settings.AimMarkersEnabled.ValueChanged += e => toggleAimMarkers(e.NewValue); - settings.AimLinesEnabled.ValueChanged += e => toggleAimLines(e.NewValue); - settings.CursorHideEnabled.ValueChanged += e => toggleCursorHidden(e.NewValue); - return settings; - } - [BackgroundDependencyLoader] private void load() { - toggleHitMarkers(AnalysisSettings.HitMarkersEnabled.Value); - toggleAimMarkers(AnalysisSettings.AimMarkersEnabled.Value); - toggleAimLines(AnalysisSettings.AimLinesEnabled.Value); - toggleCursorHidden(AnalysisSettings.CursorHideEnabled.Value); + AddInternal(Settings = new OsuAnalysisSettings()); LoadReplay(); } + protected override void LoadComplete() + { + base.LoadComplete(); + + Settings.HitMarkersEnabled.BindValueChanged(e => toggleHitMarkers(e.NewValue), true); + Settings.AimMarkersEnabled.BindValueChanged(e => toggleAimMarkers(e.NewValue), true); + Settings.AimLinesEnabled.BindValueChanged(e => toggleAimLines(e.NewValue), true); + Settings.CursorHideEnabled.BindValueChanged(e => toggleCursorHidden(e.NewValue), true); + } + private void toggleHitMarkers(bool value) => HitMarkers.FadeTo(value ? 1 : 0); private void toggleAimMarkers(bool value) => AimMarkers.FadeTo(value ? 1 : 0); private void toggleAimLines(bool value) => AimLines.FadeTo(value ? 1 : 0); - private void toggleCursorHidden(bool value) => DrawableRuleset.Playfield.Cursor.FadeTo(value ? 0 : 1); + private void toggleCursorHidden(bool value) => drawableRuleset.Playfield.Cursor.FadeTo(value ? 0 : 1); protected void LoadReplay() { bool leftHeld = false; bool rightHeld = false; - foreach (var frame in Replay.Frames) + foreach (var frame in replay.Frames) { var osuFrame = (OsuReplayFrame)frame; diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs index 6e11c87c3a..5419d4e17a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs @@ -9,13 +9,8 @@ using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { - public partial class OsuAnalysisSettings : AnalysisSettings + public partial class OsuAnalysisSettings : PlayerSettingsGroup { - public OsuAnalysisSettings(Ruleset ruleset) - : base(ruleset) - { - } - [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HitMarkersEnabled { get; } = new BindableBool(); @@ -28,10 +23,18 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool CursorHideEnabled { get; } = new BindableBool(); + public OsuAnalysisSettings() + : base("Analysis Settings") + { + } + [BackgroundDependencyLoader] private void load(IRulesetConfigCache cache) { - var config = (OsuRulesetConfigManager)cache.GetConfigFor(Ruleset)!; + AddRange(this.CreateSettingsControls()); + + var config = (OsuRulesetConfigManager)cache.GetConfigFor(new OsuRuleset())!; + config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, AimLinesEnabled); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 2e48b8e16f..5af1fd386c 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -406,7 +406,5 @@ namespace osu.Game.Rulesets /// Can be overridden to avoid showing scroll speed changes in the editor. /// public virtual bool EditorShowScrollSpeed => true; - - public virtual DifficultySection? CreateEditorDifficultySection() => null; } } diff --git a/osu.Game/Rulesets/UI/AnalysisContainer.cs b/osu.Game/Rulesets/UI/AnalysisContainer.cs deleted file mode 100644 index b6c2a8c1c8..0000000000 --- a/osu.Game/Rulesets/UI/AnalysisContainer.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Containers; -using osu.Game.Replays; -using osu.Game.Screens.Play.PlayerSettings; - -namespace osu.Game.Rulesets.UI -{ - public abstract partial class AnalysisContainer : Container - { - protected Replay Replay; - protected DrawableRuleset DrawableRuleset; - - public AnalysisSettings AnalysisSettings; - - protected AnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) - { - Replay = replay; - DrawableRuleset = drawableRuleset; - - AnalysisSettings = CreateAnalysisSettings(drawableRuleset.Ruleset); - } - - protected abstract AnalysisSettings CreateAnalysisSettings(Ruleset ruleset); - } -} diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5b1f59d549..a28b2716cb 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -596,8 +596,6 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); - - public virtual AnalysisContainer CreateAnalysisContainer(Replay replay) => null; } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e116acdc19..90a2f63faa 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -291,12 +291,6 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); - /// - /// Adds an analysis container to internal children for replays. - /// - /// - public virtual void AddAnalysisContainer(AnalysisContainer analysisContainer) => AddInternal(analysisContainer); - #region Pooling support private readonly Dictionary pools = new Dictionary(); diff --git a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs b/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs deleted file mode 100644 index 4c64eef92f..0000000000 --- a/osu.Game/Screens/Play/PlayerSettings/AnalysisSettings.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Configuration; -using osu.Game.Rulesets; - -namespace osu.Game.Screens.Play.PlayerSettings -{ - public partial class AnalysisSettings : PlayerSettingsGroup - { - protected Ruleset Ruleset; - - public AnalysisSettings(Ruleset ruleset) - : base("Analysis Settings") - { - Ruleset = ruleset; - - AddRange(this.CreateSettingsControls()); - } - } -} diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 82a2f09250..ff60dbc0d0 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -71,14 +71,6 @@ namespace osu.Game.Screens.Play playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate); HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings); - - var analysisContainer = DrawableRuleset.CreateAnalysisContainer(GameplayState.Score.Replay); - - if (analysisContainer != null) - { - HUDOverlay.PlayerSettingsOverlay.AddAtStart(analysisContainer.AnalysisSettings); - DrawableRuleset.Playfield.AddAnalysisContainer(analysisContainer); - } } protected override void PrepareReplay() From 992a0da95727c3d574289d6b3664d2be0729166c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:43:33 +0900 Subject: [PATCH 105/308] Rename classes slightly --- .../TestSceneOsuAnalysisContainer.cs | 8 ++++---- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 +- .../{OsuAnalysisContainer.cs => ReplayAnalysisOverlay.cs} | 8 ++++---- .../{OsuAnalysisSettings.cs => ReplayAnalysisSettings.cs} | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game.Rulesets.Osu/UI/{OsuAnalysisContainer.cs => ReplayAnalysisOverlay.cs} (96%) rename osu.Game.Rulesets.Osu/UI/{OsuAnalysisSettings.cs => ReplayAnalysisSettings.cs} (93%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 536de8b41a..548e40487b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Tests { public partial class TestSceneOsuAnalysisContainer : OsuTestScene { - private TestOsuAnalysisContainer analysisContainer = null!; + private TestReplayAnalysisOverlay analysisContainer = null!; [SetUpSteps] public void SetUpSteps() @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests Children = new Drawable[] { drawableRuleset, - analysisContainer = new TestOsuAnalysisContainer(fabricateReplay(), drawableRuleset), + analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay(), drawableRuleset), }; }); } @@ -96,9 +96,9 @@ namespace osu.Game.Rulesets.Osu.Tests return new Replay { Frames = frames }; } - private partial class TestOsuAnalysisContainer : OsuAnalysisContainer + private partial class TestReplayAnalysisOverlay : ReplayAnalysisOverlay { - public TestOsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) + public TestReplayAnalysisOverlay(Replay replay, DrawableRuleset drawableRuleset) : base(replay, drawableRuleset) { } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index ba0768db5d..ee16fa91f4 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override void LoadComplete() { if (HasReplayLoaded.Value) - LoadComponentAsync(new OsuAnalysisContainer(ReplayScore.Replay, this), PlayfieldAdjustmentContainer.Add); + LoadComponentAsync(new ReplayAnalysisOverlay(ReplayScore.Replay, this), PlayfieldAdjustmentContainer.Add); base.LoadComplete(); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs similarity index 96% rename from osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 19e53944c9..521f67a77c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -19,18 +19,18 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI { - public partial class OsuAnalysisContainer : CompositeDrawable + public partial class ReplayAnalysisOverlay : CompositeDrawable { protected readonly HitMarkersContainer HitMarkers; protected readonly AimMarkersContainer AimMarkers; protected readonly AimLinesContainer AimLines; - public OsuAnalysisSettings Settings = null!; + public ReplayAnalysisSettings Settings = null!; private readonly Replay replay; private readonly DrawableRuleset drawableRuleset; - public OsuAnalysisContainer(Replay replay, DrawableRuleset drawableRuleset) + public ReplayAnalysisOverlay(Replay replay, DrawableRuleset drawableRuleset) { this.replay = replay; this.drawableRuleset = drawableRuleset; @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load() { - AddInternal(Settings = new OsuAnalysisSettings()); + AddInternal(Settings = new ReplayAnalysisSettings()); LoadReplay(); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs similarity index 93% rename from osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index 5419d4e17a..87180e155b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -9,7 +9,7 @@ using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Osu.UI { - public partial class OsuAnalysisSettings : PlayerSettingsGroup + public partial class ReplayAnalysisSettings : PlayerSettingsGroup { [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HitMarkersEnabled { get; } = new BindableBool(); @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool CursorHideEnabled { get; } = new BindableBool(); - public OsuAnalysisSettings() + public ReplayAnalysisSettings() : base("Analysis Settings") { } From cc3d220f6f421603fda86800025fe98a859c7c8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 18:58:19 +0900 Subject: [PATCH 106/308] Allow settings to be added to replay HUD from ruleset --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 16 +++++++--------- .../UI/ReplayAnalysisOverlay.cs | 5 +++-- osu.Game/Screens/Play/ReplayPlayer.cs | 10 ++++++++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index ee16fa91f4..880b2dbe6f 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; @@ -31,20 +30,19 @@ namespace osu.Game.Rulesets.Osu.UI public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; - public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) : base(ruleset, beatmap, mods) { } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load(ReplayPlayer? replayPlayer) { - if (HasReplayLoaded.Value) - LoadComponentAsync(new ReplayAnalysisOverlay(ReplayScore.Replay, this), PlayfieldAdjustmentContainer.Add); - - base.LoadComplete(); + if (replayPlayer != null) + PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay, this)); } - public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) => null; + public override DrawableHitObject? CreateDrawableRepresentation(OsuHitObject h) => null; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 521f67a77c..398ab54981 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osuTK; using osuTK.Graphics; @@ -44,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load() + private void load(ReplayPlayer replayPlayer) { - AddInternal(Settings = new ReplayAnalysisSettings()); + replayPlayer.AddSettings(Settings = new ReplayAnalysisSettings()); LoadReplay(); } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index ff60dbc0d0..0c125264a1 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -55,6 +55,16 @@ namespace osu.Game.Screens.Play this.createScore = createScore; } + /// + /// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings. + /// + /// The settings group to be shown. + public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => + { + settings.Expanded.Value = false; + HUDOverlay.PlayerSettingsOverlay.Add(settings); + }); + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { From 9b81deb3ac95988d72afb96b509eb6785855281d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 19:09:23 +0900 Subject: [PATCH 107/308] Fix settings not working if `ReplayPlayer` is not available --- .../TestSceneOsuAnalysisContainer.cs | 2 ++ .../UI/ReplayAnalysisOverlay.cs | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 548e40487b..fc2255a9cf 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -103,6 +103,8 @@ namespace osu.Game.Rulesets.Osu.Tests { } + public new ReplayAnalysisSettings Settings => base.Settings; + public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 398ab54981..d16f161370 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.UI protected readonly AimMarkersContainer AimMarkers; protected readonly AimLinesContainer AimLines; - public ReplayAnalysisSettings Settings = null!; + protected ReplayAnalysisSettings Settings = null!; private readonly Replay replay; private readonly DrawableRuleset drawableRuleset; @@ -45,32 +45,30 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load(ReplayPlayer replayPlayer) + private void load(ReplayPlayer? replayPlayer) { - replayPlayer.AddSettings(Settings = new ReplayAnalysisSettings()); + Settings = new ReplayAnalysisSettings(); - LoadReplay(); + if (replayPlayer != null) + replayPlayer.AddSettings(Settings); + else + // only in test + AddInternal(Settings); + + loadReplay(); } protected override void LoadComplete() { base.LoadComplete(); - Settings.HitMarkersEnabled.BindValueChanged(e => toggleHitMarkers(e.NewValue), true); - Settings.AimMarkersEnabled.BindValueChanged(e => toggleAimMarkers(e.NewValue), true); - Settings.AimLinesEnabled.BindValueChanged(e => toggleAimLines(e.NewValue), true); - Settings.CursorHideEnabled.BindValueChanged(e => toggleCursorHidden(e.NewValue), true); + Settings.HitMarkersEnabled.BindValueChanged(enabled => HitMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + Settings.AimMarkersEnabled.BindValueChanged(enabled => AimMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + Settings.AimLinesEnabled.BindValueChanged(enabled => AimLines.FadeTo(enabled.NewValue ? 1 : 0), true); + Settings.CursorHideEnabled.BindValueChanged(enabled => drawableRuleset.Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); } - private void toggleHitMarkers(bool value) => HitMarkers.FadeTo(value ? 1 : 0); - - private void toggleAimMarkers(bool value) => AimMarkers.FadeTo(value ? 1 : 0); - - private void toggleAimLines(bool value) => AimLines.FadeTo(value ? 1 : 0); - - private void toggleCursorHidden(bool value) => drawableRuleset.Playfield.Cursor.FadeTo(value ? 0 : 1); - - protected void LoadReplay() + private void loadReplay() { bool leftHeld = false; bool rightHeld = false; From 6c07b873af174461888006d87420264b8f349109 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 19:28:07 +0900 Subject: [PATCH 108/308] Isolate configuration container from analysis overlay --- .../TestSceneOsuAnalysisContainer.cs | 32 ++++++++--------- .../Configuration/OsuRulesetConfigManager.cs | 4 +-- .../UI/DrawableOsuRuleset.cs | 12 +++++-- .../UI/ReplayAnalysisOverlay.cs | 35 ++++++++----------- .../UI/ReplayAnalysisSettings.cs | 9 ++--- 5 files changed, 47 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index fc2255a9cf..04fcb36cad 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -5,14 +5,14 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Replays; -using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.UI; using osu.Game.Tests.Visual; using osuTK; @@ -21,18 +21,20 @@ namespace osu.Game.Rulesets.Osu.Tests public partial class TestSceneOsuAnalysisContainer : OsuTestScene { private TestReplayAnalysisOverlay analysisContainer = null!; + private ReplayAnalysisSettings settings = null!; + + [Cached] + private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo); [SetUpSteps] public void SetUpSteps() { AddStep("create analysis container", () => { - DrawableOsuRuleset drawableRuleset = new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()); - Children = new Drawable[] { - drawableRuleset, - analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay(), drawableRuleset), + analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), + settings = new ReplayAnalysisSettings(config), }; }); } @@ -40,27 +42,27 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestHitMarkers() { - AddStep("enable hit markers", () => analysisContainer.Settings.HitMarkersEnabled.Value = true); + AddStep("enable hit markers", () => settings.HitMarkersEnabled.Value = true); AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); - AddStep("disable hit markers", () => analysisContainer.Settings.HitMarkersEnabled.Value = false); + AddStep("disable hit markers", () => settings.HitMarkersEnabled.Value = false); AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); } [Test] public void TestAimMarker() { - AddStep("enable aim markers", () => analysisContainer.Settings.AimMarkersEnabled.Value = true); + AddStep("enable aim markers", () => settings.AimMarkersEnabled.Value = true); AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); - AddStep("disable aim markers", () => analysisContainer.Settings.AimMarkersEnabled.Value = false); + AddStep("disable aim markers", () => settings.AimMarkersEnabled.Value = false); AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); } [Test] public void TestAimLines() { - AddStep("enable aim lines", () => analysisContainer.Settings.AimLinesEnabled.Value = true); + AddStep("enable aim lines", () => settings.AimLinesEnabled.Value = true); AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); - AddStep("disable aim lines", () => analysisContainer.Settings.AimLinesEnabled.Value = false); + AddStep("disable aim lines", () => settings.AimLinesEnabled.Value = false); AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); } @@ -98,13 +100,11 @@ namespace osu.Game.Rulesets.Osu.Tests private partial class TestReplayAnalysisOverlay : ReplayAnalysisOverlay { - public TestReplayAnalysisOverlay(Replay replay, DrawableRuleset drawableRuleset) - : base(replay, drawableRuleset) + public TestReplayAnalysisOverlay(Replay replay) + : base(replay) { } - public new ReplayAnalysisSettings Settings => base.Settings; - public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index 23b7b9c1fa..df5cd55c33 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Game.Configuration; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.UI; @@ -11,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Configuration { public class OsuRulesetConfigManager : RulesetConfigManager { - public OsuRulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null) + public OsuRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null) : base(settings, ruleset, variant) { } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 880b2dbe6f..09dcd54c3c 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; @@ -24,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class DrawableOsuRuleset : DrawableRuleset { - protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; + private Bindable? cursorHideEnabled; public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager; @@ -39,7 +41,13 @@ namespace osu.Game.Rulesets.Osu.UI private void load(ReplayPlayer? replayPlayer) { if (replayPlayer != null) - PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay, this)); + { + PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); + replayPlayer.AddSettings(new ReplayAnalysisSettings((OsuRulesetConfigManager)Config)); + + cursorHideEnabled = ((OsuRulesetConfigManager)Config).GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); + cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); + } } public override DrawableHitObject? CreateDrawableRepresentation(OsuHitObject h) => null; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index d16f161370..80fb561ed1 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; @@ -11,10 +12,9 @@ using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; using osu.Game.Replays; using osu.Game.Rulesets.Objects.Pooling; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Skinning.Default; -using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using osuTK; using osuTK.Graphics; @@ -22,19 +22,19 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class ReplayAnalysisOverlay : CompositeDrawable { + private BindableBool hitMarkersEnabled { get; } = new BindableBool(); + private BindableBool aimMarkersEnabled { get; } = new BindableBool(); + private BindableBool aimLinesEnabled { get; } = new BindableBool(); + protected readonly HitMarkersContainer HitMarkers; protected readonly AimMarkersContainer AimMarkers; protected readonly AimLinesContainer AimLines; - protected ReplayAnalysisSettings Settings = null!; - private readonly Replay replay; - private readonly DrawableRuleset drawableRuleset; - public ReplayAnalysisOverlay(Replay replay, DrawableRuleset drawableRuleset) + public ReplayAnalysisOverlay(Replay replay) { this.replay = replay; - this.drawableRuleset = drawableRuleset; InternalChildren = new Drawable[] { @@ -45,27 +45,22 @@ namespace osu.Game.Rulesets.Osu.UI } [BackgroundDependencyLoader] - private void load(ReplayPlayer? replayPlayer) + private void load(OsuRulesetConfigManager config) { - Settings = new ReplayAnalysisSettings(); - - if (replayPlayer != null) - replayPlayer.AddSettings(Settings); - else - // only in test - AddInternal(Settings); - loadReplay(); + + config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, hitMarkersEnabled); + config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, aimMarkersEnabled); + config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, aimLinesEnabled); } protected override void LoadComplete() { base.LoadComplete(); - Settings.HitMarkersEnabled.BindValueChanged(enabled => HitMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - Settings.AimMarkersEnabled.BindValueChanged(enabled => AimMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - Settings.AimLinesEnabled.BindValueChanged(enabled => AimLines.FadeTo(enabled.NewValue ? 1 : 0), true); - Settings.CursorHideEnabled.BindValueChanged(enabled => drawableRuleset.Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); + hitMarkersEnabled.BindValueChanged(enabled => HitMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimMarkersEnabled.BindValueChanged(enabled => AimMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimLinesEnabled.BindValueChanged(enabled => AimLines.FadeTo(enabled.NewValue ? 1 : 0), true); } private void loadReplay() diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index 87180e155b..dd09ee146b 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class ReplayAnalysisSettings : PlayerSettingsGroup { + private readonly OsuRulesetConfigManager config; + [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HitMarkersEnabled { get; } = new BindableBool(); @@ -23,18 +25,17 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool CursorHideEnabled { get; } = new BindableBool(); - public ReplayAnalysisSettings() + public ReplayAnalysisSettings(OsuRulesetConfigManager config) : base("Analysis Settings") { + this.config = config; } [BackgroundDependencyLoader] - private void load(IRulesetConfigCache cache) + private void load() { AddRange(this.CreateSettingsControls()); - var config = (OsuRulesetConfigManager)cache.GetConfigFor(new OsuRuleset())!; - config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, AimLinesEnabled); From abd74ab41cb8e2b6b34c0786bc88105f0b4378f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 19:41:40 +0900 Subject: [PATCH 109/308] Add note about being able to apply a newer update during runtime --- osu.Desktop/Updater/VelopackUpdateManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index 527892413a..e550755fff 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -54,6 +54,8 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; + // TODO: we should probably be checking if there's a more recent update, rather than shortcutting here. + // Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975). if (pendingUpdate != null) { // If there is an update pending restart, show the notification to restart again. From 6a309725ed62370accbf45784d15a57af52c00d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 19:50:45 +0900 Subject: [PATCH 110/308] Make test more usable --- .../TestSceneOsuAnalysisContainer.cs | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 04fcb36cad..d31b27e00d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -33,9 +33,27 @@ namespace osu.Game.Rulesets.Osu.Tests { Children = new Drawable[] { - analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), + new OsuPlayfieldAdjustmentContainer + { + Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()), + }, settings = new ReplayAnalysisSettings(config), }; + + settings.HitMarkersEnabled.Value = false; + settings.AimMarkersEnabled.Value = false; + settings.AimLinesEnabled.Value = false; + }); + } + + [Test] + public void TestEverythingOn() + { + AddStep("enable everything", () => + { + settings.HitMarkersEnabled.Value = true; + settings.AimMarkersEnabled.Value = true; + settings.AimLinesEnabled.Value = true; }); } @@ -76,8 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests for (int i = 0; i < 1000; i++) { - posX = Math.Clamp(posX + random.Next(-10, 11), 0, 500); - posY = Math.Clamp(posY + random.Next(-10, 11), 0, 500); + posX = Math.Clamp(posX + random.Next(-20, 21), 0, 500); + posY = Math.Clamp(posY + random.Next(-20, 21), 0, 500); var actions = new List(); From b7a56c8a4587fd197624c27ef8fba367edc8bebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 Sep 2024 13:57:53 +0200 Subject: [PATCH 111/308] Implement "form" text box control --- .../UserInterface/TestSceneFormControls.cs | 61 +++++ .../UserInterface/ThemeComparisonTestScene.cs | 2 +- .../UserInterfaceV2/FormFieldCaption.cs | 68 +++++ .../Graphics/UserInterfaceV2/FormNumberBox.cs | 23 ++ .../Graphics/UserInterfaceV2/FormTextBox.cs | 238 ++++++++++++++++++ 5 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs new file mode 100644 index 0000000000..f5bc40c869 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneFormControls : ThemeComparisonTestScene + { + public TestSceneFormControls() + : base(false) + { + } + + protected override Drawable CreateContent() => new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new FormTextBox + { + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + TabbableContentContainer = this, + }, + new FormTextBox + { + Caption = "Artist", + HintText = "Poot artist here!", + PlaceholderText = "Here is an artist", + Current = { Disabled = true }, + TabbableContentContainer = this, + }, + new FormNumberBox + { + Caption = "Number", + HintText = "Insert your favourite number", + PlaceholderText = "Mine is 42!", + TabbableContentContainer = this, + }, + }, + }, + }, + }; + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs index 4700ef72d9..44133d89f8 100644 --- a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs +++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 + Colour = colourProvider.Background3 }, CreateContent() } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs b/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs new file mode 100644 index 0000000000..75c27618e9 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormFieldCaption.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormFieldCaption : CompositeDrawable, IHasTooltip + { + private LocalisableString caption; + + public LocalisableString Caption + { + get => caption; + set + { + caption = value; + + if (captionText.IsNotNull()) + captionText.Text = value; + } + } + + private OsuSpriteText captionText = null!; + + public LocalisableString TooltipText { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + captionText = new OsuSpriteText + { + Text = caption, + Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Alpha = TooltipText == default ? 0 : 1, + Size = new Vector2(10), + Icon = FontAwesome.Solid.QuestionCircle, + Margin = new MarginPadding { Top = 1, }, + } + }, + }; + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.cs new file mode 100644 index 0000000000..66f1a45210 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormNumberBox.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.Graphics.UserInterfaceV2 +{ + public partial class FormNumberBox : FormTextBox + { + public bool AllowDecimals { get; init; } + + internal override InnerTextBox CreateTextBox() => new InnerNumberBox + { + AllowDecimals = AllowDecimals, + }; + + internal partial class InnerNumberBox : InnerTextBox + { + public bool AllowDecimals { get; init; } + + protected override bool CanAddCharacter(char character) + => char.IsAsciiDigit(character) || (AllowDecimals && character == '.'); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs new file mode 100644 index 0000000000..985eb74662 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -0,0 +1,238 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +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.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormTextBox : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private bool readOnly; + + public bool ReadOnly + { + get => readOnly; + set + { + readOnly = value; + + if (textBox.IsNotNull()) + updateState(); + } + } + + private CompositeDrawable? tabbableContentContainer; + + public CompositeDrawable? TabbableContentContainer + { + set + { + tabbableContentContainer = value; + + if (textBox.IsNotNull()) + textBox.TabbableContentContainer = tabbableContentContainer; + } + } + + public event TextBox.OnCommitHandler? OnCommit; + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public LocalisableString Caption { get; init; } + public LocalisableString HintText { get; init; } + public LocalisableString PlaceholderText { get; init; } + + private Box background = null!; + private Box flashLayer = null!; + private InnerTextBox textBox = null!; + private FormFieldCaption caption = null!; + private IFocusManager focusManager = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Transparent, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + textBox = CreateTextBox().With(t => + { + t.Anchor = Anchor.BottomRight; + t.Origin = Anchor.BottomRight; + t.RelativeSizeAxes = Axes.X; + t.Width = 1; + t.PlaceholderText = PlaceholderText; + t.Current = Current; + t.CommitOnFocusLost = true; + t.OnCommit += (textBox, newText) => + { + OnCommit?.Invoke(textBox, newText); + + if (!current.Disabled && !ReadOnly) + { + flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark1.Opacity(0), colourProvider.Dark2); + flashLayer.FadeOutFromOne(800, Easing.OutQuint); + } + }; + t.OnInputError = () => + { + flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); + flashLayer.FadeOutFromOne(200, Easing.OutQuint); + }; + t.TabbableContentContainer = tabbableContentContainer; + }), + }, + }, + }; + } + + internal virtual InnerTextBox CreateTextBox() => new InnerTextBox(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + focusManager = GetContainingFocusManager()!; + textBox.Focused.BindValueChanged(_ => updateState()); + current.BindDisabledChanged(_ => updateState(), true); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + focusManager.ChangeFocus(textBox); + return true; + } + + private void updateState() + { + bool disabled = Current.Disabled || ReadOnly; + + textBox.ReadOnly = disabled; + textBox.Alpha = 1; + + caption.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content2; + textBox.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + if (!disabled) + { + BorderThickness = IsHovered || textBox.Focused.Value ? 3 : 0; + BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; + + if (textBox.Focused.Value) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + background.Colour = colourProvider.Background5; + } + else + { + background.Colour = colourProvider.Background4; + } + } + + internal partial class InnerTextBox : OsuTextBox + { + public BindableBool Focused { get; } = new BindableBool(); + + public Action? OnInputError { get; set; } + + protected override float LeftRightPadding => 0; + + [BackgroundDependencyLoader] + private void load() + { + Height = 16; + TextContainer.Height = 1; + Masking = false; + BackgroundUnfocused = BackgroundFocused = BackgroundCommit = Colour4.Transparent; + } + + protected override SpriteText CreatePlaceholder() => base.CreatePlaceholder().With(t => t.Margin = default); + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + Focused.Value = true; + } + + protected override void OnFocusLost(FocusLostEvent e) + { + base.OnFocusLost(e); + + Focused.Value = false; + } + + protected override void NotifyInputError() + { + PlayFeedbackSample(FeedbackSampleType.TextInvalid); + // base call intentionally suppressed + OnInputError?.Invoke(); + } + } + } +} From 17760afa6023a9eca18051b4a1e2f22ed0f131aa Mon Sep 17 00:00:00 2001 From: Fabep Date: Wed, 4 Sep 2024 15:29:48 +0200 Subject: [PATCH 112/308] Changed ModCustomisationHeader to inherit from OsuClickableContainer. ModCustomisationHeader changes color depending on state. --- .../Overlays/Mods/ModCustomisationHeader.cs | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 32fd5a37aa..1d40fb3f5c 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -20,17 +19,16 @@ using static osu.Game.Overlays.Mods.ModCustomisationPanel; namespace osu.Game.Overlays.Mods { - public partial class ModCustomisationHeader : OsuHoverContainer + public partial class ModCustomisationHeader : OsuClickableContainer { private Box background = null!; + private Box hoverBackground = null!; private Box backgroundFlash = null!; private SpriteIcon icon = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - protected override IEnumerable EffectTargets => new[] { background }; - public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); private readonly ModCustomisationPanel panel; @@ -53,6 +51,13 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.Both, }, + hoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(80).Opacity(180), + Blending = BlendingParameters.Additive, + Alpha = 0, + }, backgroundFlash = new Box { RelativeSizeAxes = Axes.Both, @@ -84,9 +89,6 @@ namespace osu.Game.Overlays.Mods } } }; - - IdleColour = colourProvider.Dark3; - HoverColour = colourProvider.Light4; } protected override void LoadComplete() @@ -109,15 +111,40 @@ namespace osu.Game.Overlays.Mods ExpandedState.BindValueChanged(v => { icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); + + switch (v.NewValue) + { + case ModCustomisationPanelState.Collapsed: + background.FadeColour(colourProvider.Dark3, 500, Easing.OutQuint); + break; + + case ModCustomisationPanelState.Expanded: + case ModCustomisationPanelState.ExpandedByMod: + background.FadeColour(colourProvider.Light4, 500, Easing.OutQuint); + break; + } }, true); } protected override bool OnHover(HoverEvent e) { - if (Enabled.Value && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) + if (!Enabled.Value) + return base.OnHover(e); + + if (panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; + hoverBackground.FadeIn(200); + return base.OnHover(e); } + + protected override void OnHoverLost(HoverLostEvent e) + { + if (Enabled.Value) + hoverBackground.FadeOut(200); + + base.OnHoverLost(e); + } } } From 6fc60908c01816a986792e31918de381944342e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 01:00:23 +0900 Subject: [PATCH 113/308] Trigger request failure on receiving a null response for a typed `APIRequest` --- osu.Game/Online/API/APIRequest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 45ebbcd76d..5cbe9040ba 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -46,6 +46,9 @@ namespace osu.Game.Online.API Response = ((OsuJsonWebRequest)WebRequest).ResponseObject; Logger.Log($"{GetType().ReadableName()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); } + + if (Response == null) + TriggerFailure(new ArgumentNullException(nameof(Response))); } internal void TriggerSuccess(T result) @@ -152,6 +155,8 @@ namespace osu.Game.Online.API PostProcess(); + if (isFailing) return; + TriggerSuccess(); } From dcb463acafddefe68c2ed90fab193f599c9458f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 20:07:31 +0900 Subject: [PATCH 114/308] Split out classes and avoid weird configuration stuff --- .../Skinning/Default/HitMarker.cs | 74 -------- .../UI/ReplayAnalysis/AimLinesContainer.cs | 70 ++++++++ .../UI/ReplayAnalysis/AimMarkersContainer.cs | 20 +++ .../UI/ReplayAnalysis/AimPointEntry.cs | 20 +++ .../UI/ReplayAnalysis/HitMarker.cs | 27 +++ .../UI/ReplayAnalysis/HitMarkerEntry.cs | 18 ++ .../UI/ReplayAnalysis/HitMarkerLeftClick.cs | 49 ++++++ .../UI/ReplayAnalysis/HitMarkerMovement.cs | 50 ++++++ .../UI/ReplayAnalysis/HitMarkerRightClick.cs | 49 ++++++ .../UI/ReplayAnalysis/HitMarkersContainer.cs | 28 +++ .../UI/ReplayAnalysisOverlay.cs | 160 +----------------- 11 files changed, 334 insertions(+), 231 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs deleted file mode 100644 index fe662470bc..0000000000 --- a/osu.Game.Rulesets.Osu/Skinning/Default/HitMarker.cs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Skinning.Default -{ - public partial class HitMarker : CompositeDrawable - { - public HitMarker(OsuAction? action) - { - var (colour, length, hasBorder) = getConfig(action); - - if (hasBorder) - { - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) - } - }; - } - - AddRangeInternal(new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Colour = colour - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - Colour = colour - } - }); - } - - private (Colour4 colour, float length, bool hasBorder) getConfig(OsuAction? action) - { - switch (action) - { - case OsuAction.LeftButton: - return (Colour4.Orange, 20, true); - - case OsuAction.RightButton: - return (Colour4.LightGreen, 20, true); - - default: - return (Colour4.Gray.Opacity(0.5F), 8, false); - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs new file mode 100644 index 0000000000..db747e2775 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics.Lines; +using osu.Framework.Graphics.Performance; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class AimLinesContainer : Path + { + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); + + public AimLinesContainer() + { + lifetimeManager.EntryBecameAlive += entryBecameAlive; + lifetimeManager.EntryBecameDead += entryBecameDead; + + PathRadius = 1f; + Colour = new Color4(255, 255, 255, 127); + } + + protected override void Update() + { + base.Update(); + + lifetimeManager.Update(Time.Current); + } + + public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); + + public void Clear() => lifetimeManager.ClearEntries(); + + private void entryBecameAlive(LifetimeEntry entry) + { + aliveEntries.Add((AimPointEntry)entry); + updateVertices(); + } + + private void entryBecameDead(LifetimeEntry entry) + { + aliveEntries.Remove((AimPointEntry)entry); + updateVertices(); + } + + private void updateVertices() + { + ClearVertices(); + + foreach (var entry in aliveEntries) + { + AddVertex(entry.Position); + } + } + + private sealed class AimLinePointComparator : IComparer + { + public int Compare(AimPointEntry? x, AimPointEntry? y) + { + ArgumentNullException.ThrowIfNull(x); + ArgumentNullException.ThrowIfNull(y); + + return x.LifetimeStart.CompareTo(y.LifetimeStart); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs new file mode 100644 index 0000000000..76e88ad5b0 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer + { + private readonly DrawablePool pool; + + public AimMarkersContainer() + { + AddInternal(pool = new DrawablePool(80)); + } + + protected override HitMarker GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs new file mode 100644 index 0000000000..948568eb12 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Performance; +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class AimPointEntry : LifetimeEntry + { + public Vector2 Position { get; } + + public AimPointEntry(double time, Vector2 position) + { + LifetimeStart = time; + LifetimeEnd = time + 1_000; + Position = position; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs new file mode 100644 index 0000000000..339bdae5da --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarker : PoolableDrawableWithLifetime + { + public HitMarker() + { + Origin = Anchor.Centre; + } + + protected override void OnApply(AimPointEntry entry) + { + Position = entry.Position; + + using (BeginAbsoluteSequence(LifetimeStart)) + Show(); + + using (BeginAbsoluteSequence(LifetimeEnd - 200)) + this.FadeOut(200); + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs new file mode 100644 index 0000000000..80c268910d --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkerEntry : AimPointEntry + { + public bool IsLeftMarker { get; } + + public HitMarkerEntry(double lifetimeStart, Vector2 position, bool isLeftMarker) + : base(lifetimeStart, position) + { + IsLeftMarker = isLeftMarker; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs new file mode 100644 index 0000000000..988fc28371 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs @@ -0,0 +1,49 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkerLeftClick : HitMarker + { + public HitMarkerLeftClick() + { + const float length = 20; + + Colour = Color4.OrangeRed; + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 90, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 90, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs new file mode 100644 index 0000000000..4f9b6f8790 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs @@ -0,0 +1,50 @@ +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkerMovement : HitMarker + { + public HitMarkerMovement() + { + const float length = 5; + + Colour = Color4.Gray.Opacity(0.4f); + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 90, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 90, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs new file mode 100644 index 0000000000..32cdd2d0b5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs @@ -0,0 +1,49 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkerRightClick : HitMarker + { + public HitMarkerRightClick() + { + const float length = 20; + + Colour = Color4.GreenYellow; + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(3, length), + Rotation = 90, + Colour = Colour4.Black.Opacity(0.5F) + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(1, length), + Rotation = 90, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs new file mode 100644 index 0000000000..fcc5fa28c2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer + { + private readonly DrawablePool leftPool; + private readonly DrawablePool rightPool; + + public HitMarkersContainer() + { + AddInternal(leftPool = new DrawablePool(15)); + AddInternal(rightPool = new DrawablePool(15)); + } + + protected override HitMarker GetDrawable(HitMarkerEntry entry) + { + if (entry.IsLeftMarker) + return leftPool.Get(d => d.Apply(entry)); + + return rightPool.Get(d => d.Apply(entry)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 80fb561ed1..2cc1948474 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -1,22 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.Performance; -using osu.Framework.Graphics.Pooling; using osu.Game.Replays; -using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Replays; -using osu.Game.Rulesets.Osu.Skinning.Default; -using osuTK; -using osuTK.Graphics; +using osu.Game.Rulesets.Osu.UI.ReplayAnalysis; namespace osu.Game.Rulesets.Osu.UI { @@ -34,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.UI public ReplayAnalysisOverlay(Replay replay) { + RelativeSizeAxes = Axes.Both; + this.replay = replay; InternalChildren = new Drawable[] @@ -95,153 +89,5 @@ namespace osu.Game.Rulesets.Osu.UI } } } - - protected partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer - { - private readonly HitMarkerPool leftPool; - private readonly HitMarkerPool rightPool; - - public HitMarkersContainer() - { - AddInternal(leftPool = new HitMarkerPool(OsuAction.LeftButton, 15)); - AddInternal(rightPool = new HitMarkerPool(OsuAction.RightButton, 15)); - } - - protected override HitMarkerDrawable GetDrawable(HitMarkerEntry entry) => (entry.IsLeftMarker ? leftPool : rightPool).Get(d => d.Apply(entry)); - } - - protected partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer - { - private readonly HitMarkerPool pool; - - public AimMarkersContainer() - { - AddInternal(pool = new HitMarkerPool(null, 80)); - } - - protected override HitMarkerDrawable GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); - } - - protected partial class AimLinesContainer : Path - { - private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); - - public AimLinesContainer() - { - lifetimeManager.EntryBecameAlive += entryBecameAlive; - lifetimeManager.EntryBecameDead += entryBecameDead; - - PathRadius = 1f; - Colour = new Color4(255, 255, 255, 127); - } - - protected override void Update() - { - base.Update(); - - lifetimeManager.Update(Time.Current); - } - - public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); - - public void Clear() => lifetimeManager.ClearEntries(); - - private void entryBecameAlive(LifetimeEntry entry) - { - aliveEntries.Add((AimPointEntry)entry); - updateVertices(); - } - - private void entryBecameDead(LifetimeEntry entry) - { - aliveEntries.Remove((AimPointEntry)entry); - updateVertices(); - } - - private void updateVertices() - { - ClearVertices(); - - foreach (var entry in aliveEntries) - { - AddVertex(entry.Position); - } - } - - private sealed class AimLinePointComparator : IComparer - { - public int Compare(AimPointEntry? x, AimPointEntry? y) - { - ArgumentNullException.ThrowIfNull(x); - ArgumentNullException.ThrowIfNull(y); - - return x.LifetimeStart.CompareTo(y.LifetimeStart); - } - } - } - - protected partial class HitMarkerDrawable : PoolableDrawableWithLifetime - { - /// - /// This constructor only exists to meet the new() type constraint of . - /// - public HitMarkerDrawable() - { - } - - public HitMarkerDrawable(OsuAction? action) - { - Origin = Anchor.Centre; - InternalChild = new HitMarker(action); - } - - protected override void OnApply(AimPointEntry entry) - { - Position = entry.Position; - - using (BeginAbsoluteSequence(LifetimeStart)) - Show(); - - using (BeginAbsoluteSequence(LifetimeEnd - 200)) - this.FadeOut(200); - } - } - - protected partial class HitMarkerPool : DrawablePool - { - private readonly OsuAction? action; - - public HitMarkerPool(OsuAction? action, int initialSize) - : base(initialSize) - { - this.action = action; - } - - protected override HitMarkerDrawable CreateNewDrawable() => new HitMarkerDrawable(action); - } - - protected partial class AimPointEntry : LifetimeEntry - { - public Vector2 Position { get; } - - public AimPointEntry(double time, Vector2 position) - { - LifetimeStart = time; - LifetimeEnd = time + 1_000; - Position = position; - } - } - - protected partial class HitMarkerEntry : AimPointEntry - { - public bool IsLeftMarker { get; } - - public HitMarkerEntry(double lifetimeStart, Vector2 position, bool isLeftMarker) - : base(lifetimeStart, position) - { - IsLeftMarker = isLeftMarker; - } - } } } From 7f9a98a7aa01307c15ee48d3d74408482b58d6a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 20:17:22 +0900 Subject: [PATCH 115/308] More renames --- .../TestSceneOsuAnalysisContainer.cs | 6 ++--- ...rsContainer.cs => ClickMarkerContainer.cs} | 4 +-- ...ontainer.cs => MovementMarkerContainer.cs} | 4 +-- ...sContainer.cs => MovementPathContainer.cs} | 6 ++--- .../UI/ReplayAnalysisOverlay.cs | 26 +++++++++---------- 5 files changed, 22 insertions(+), 24 deletions(-) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarkersContainer.cs => ClickMarkerContainer.cs} (85%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{AimMarkersContainer.cs => MovementMarkerContainer.cs} (78%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{AimLinesContainer.cs => MovementPathContainer.cs} (92%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index d31b27e00d..fb6d59ddba 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -123,9 +123,9 @@ namespace osu.Game.Rulesets.Osu.Tests { } - public bool HitMarkersVisible => HitMarkers.Alpha > 0 && HitMarkers.Entries.Any(); - public bool AimMarkersVisible => AimMarkers.Alpha > 0 && AimMarkers.Entries.Any(); - public bool AimLinesVisible => AimLines.Alpha > 0 && AimLines.Vertices.Count > 1; + public bool HitMarkersVisible => ClickMarkers.Alpha > 0 && ClickMarkers.Entries.Any(); + public bool AimMarkersVisible => MovementMarkers.Alpha > 0 && MovementMarkers.Entries.Any(); + public bool AimLinesVisible => MovementPath.Alpha > 0 && MovementPath.Vertices.Count > 1; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs similarity index 85% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs index fcc5fa28c2..be56e26caf 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkersContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs @@ -6,12 +6,12 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarkersContainer : PooledDrawableWithLifetimeContainer + public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer { private readonly DrawablePool leftPool; private readonly DrawablePool rightPool; - public HitMarkersContainer() + public ClickMarkerContainer() { AddInternal(leftPool = new DrawablePool(15)); AddInternal(rightPool = new DrawablePool(15)); diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs similarity index 78% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs index 76e88ad5b0..8c2bdd5908 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimMarkersContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs @@ -6,11 +6,11 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class AimMarkersContainer : PooledDrawableWithLifetimeContainer + public partial class MovementMarkerContainer : PooledDrawableWithLifetimeContainer { private readonly DrawablePool pool; - public AimMarkersContainer() + public MovementMarkerContainer() { AddInternal(pool = new DrawablePool(80)); } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs similarity index 92% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index db747e2775..58c5993f34 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimLinesContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -9,12 +9,12 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class AimLinesContainer : Path + public partial class MovementPathContainer : Path { private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); - public AimLinesContainer() + public MovementPathContainer() { lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; @@ -32,8 +32,6 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); - public void Clear() => lifetimeManager.ClearEntries(); - private void entryBecameAlive(LifetimeEntry entry) { aliveEntries.Add((AimPointEntry)entry); diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 2cc1948474..d33d468c7e 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -18,9 +18,9 @@ namespace osu.Game.Rulesets.Osu.UI private BindableBool aimMarkersEnabled { get; } = new BindableBool(); private BindableBool aimLinesEnabled { get; } = new BindableBool(); - protected readonly HitMarkersContainer HitMarkers; - protected readonly AimMarkersContainer AimMarkers; - protected readonly AimLinesContainer AimLines; + protected readonly ClickMarkerContainer ClickMarkers; + protected readonly MovementMarkerContainer MovementMarkers; + protected readonly MovementPathContainer MovementPath; private readonly Replay replay; @@ -32,9 +32,9 @@ namespace osu.Game.Rulesets.Osu.UI InternalChildren = new Drawable[] { - HitMarkers = new HitMarkersContainer(), - AimLines = new AimLinesContainer(), - AimMarkers = new AimMarkersContainer(), + ClickMarkers = new ClickMarkerContainer(), + MovementPath = new MovementPathContainer(), + MovementMarkers = new MovementMarkerContainer(), }; } @@ -52,9 +52,9 @@ namespace osu.Game.Rulesets.Osu.UI { base.LoadComplete(); - hitMarkersEnabled.BindValueChanged(enabled => HitMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimMarkersEnabled.BindValueChanged(enabled => AimMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimLinesEnabled.BindValueChanged(enabled => AimLines.FadeTo(enabled.NewValue ? 1 : 0), true); + hitMarkersEnabled.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimMarkersEnabled.BindValueChanged(enabled => MovementMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimLinesEnabled.BindValueChanged(enabled => MovementPath.FadeTo(enabled.NewValue ? 1 : 0), true); } private void loadReplay() @@ -66,8 +66,8 @@ namespace osu.Game.Rulesets.Osu.UI { var osuFrame = (OsuReplayFrame)frame; - AimMarkers.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); - AimLines.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + MovementMarkers.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + MovementPath.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.UI leftHeld = false; else if (!leftHeld && leftButton) { - HitMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); + ClickMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); leftHeld = true; } @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.UI rightHeld = false; else if (!rightHeld && rightButton) { - HitMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); + ClickMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); rightHeld = true; } } From a4a37c5ba64773ef5cae4626ca76220ca66ce87d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 20:25:29 +0900 Subject: [PATCH 116/308] Simplify lifetime entries and stuff --- ...AimPointEntry.cs => AnalysisFrameEntry.cs} | 7 ++- .../UI/ReplayAnalysis/ClickMarkerContainer.cs | 16 ++---- .../UI/ReplayAnalysis/HitMarker.cs | 4 +- ...itMarkerLeftClick.cs => HitMarkerClick.cs} | 37 +++++++++++--- .../UI/ReplayAnalysis/HitMarkerEntry.cs | 18 ------- .../UI/ReplayAnalysis/HitMarkerRightClick.cs | 49 ------------------- .../ReplayAnalysis/MovementMarkerContainer.cs | 4 +- .../ReplayAnalysis/MovementPathContainer.cs | 22 ++++++--- .../UI/ReplayAnalysisOverlay.cs | 8 +-- 9 files changed, 61 insertions(+), 104 deletions(-) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{AimPointEntry.cs => AnalysisFrameEntry.cs} (66%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarkerLeftClick.cs => HitMarkerClick.cs} (55%) delete mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs delete mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs similarity index 66% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs index 948568eb12..ca11e6aff1 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AimPointEntry.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs @@ -6,15 +6,18 @@ using osuTK; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class AimPointEntry : LifetimeEntry + public partial class AnalysisFrameEntry : LifetimeEntry { + public OsuAction? Action { get; } + public Vector2 Position { get; } - public AimPointEntry(double time, Vector2 position) + public AnalysisFrameEntry(double time, Vector2 position, OsuAction? action = null) { LifetimeStart = time; LifetimeEnd = time + 1_000; Position = position; + Action = action; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs index be56e26caf..3de1a70d7c 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs @@ -6,23 +6,15 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer + public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer { - private readonly DrawablePool leftPool; - private readonly DrawablePool rightPool; + private readonly DrawablePool clickMarkerPool; public ClickMarkerContainer() { - AddInternal(leftPool = new DrawablePool(15)); - AddInternal(rightPool = new DrawablePool(15)); + AddInternal(clickMarkerPool = new DrawablePool(30)); } - protected override HitMarker GetDrawable(HitMarkerEntry entry) - { - if (entry.IsLeftMarker) - return leftPool.Get(d => d.Apply(entry)); - - return rightPool.Get(d => d.Apply(entry)); - } + protected override HitMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry)); } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs index 339bdae5da..e2ecba44fc 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs @@ -6,14 +6,14 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarker : PoolableDrawableWithLifetime + public partial class HitMarker : PoolableDrawableWithLifetime { public HitMarker() { Origin = Anchor.Centre; } - protected override void OnApply(AimPointEntry entry) + protected override void OnApply(AnalysisFrameEntry entry) { Position = entry.Position; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs similarity index 55% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs index 988fc28371..4652ea3416 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerLeftClick.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs @@ -1,17 +1,18 @@ +using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarkerLeftClick : HitMarker + public partial class HitMarkerClick : HitMarker { - public HitMarkerLeftClick() + public HitMarkerClick() { const float length = 20; - - Colour = Color4.OrangeRed; + const float border_size = 3; InternalChildren = new Drawable[] { @@ -19,14 +20,14 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(3, length), + Size = new Vector2(border_size, length + border_size), Colour = Colour4.Black.Opacity(0.5F) }, new Box { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(3, length), + Size = new Vector2(border_size, length + border_size), Rotation = 90, Colour = Colour4.Black.Opacity(0.5F) }, @@ -45,5 +46,27 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis } }; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected override void OnApply(AnalysisFrameEntry entry) + { + base.OnApply(entry); + + switch (entry.Action) + { + case OsuAction.LeftButton: + Colour = colours.BlueLight; + break; + + case OsuAction.RightButton: + Colour = colours.YellowLight; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs deleted file mode 100644 index 80c268910d..0000000000 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerEntry.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK; - -namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis -{ - public partial class HitMarkerEntry : AimPointEntry - { - public bool IsLeftMarker { get; } - - public HitMarkerEntry(double lifetimeStart, Vector2 position, bool isLeftMarker) - : base(lifetimeStart, position) - { - IsLeftMarker = isLeftMarker; - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs deleted file mode 100644 index 32cdd2d0b5..0000000000 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerRightClick.cs +++ /dev/null @@ -1,49 +0,0 @@ -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis -{ - public partial class HitMarkerRightClick : HitMarker - { - public HitMarkerRightClick() - { - const float length = 20; - - Colour = Color4.GreenYellow; - - InternalChildren = new Drawable[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - } - }; - } - } -} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs index 8c2bdd5908..d52f54650d 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class MovementMarkerContainer : PooledDrawableWithLifetimeContainer + public partial class MovementMarkerContainer : PooledDrawableWithLifetimeContainer { private readonly DrawablePool pool; @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis AddInternal(pool = new DrawablePool(80)); } - protected override HitMarker GetDrawable(AimPointEntry entry) => pool.Get(d => d.Apply(entry)); + protected override HitMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry)); } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index 58c5993f34..1d52157036 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -3,16 +3,17 @@ using System; using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; -using osuTK.Graphics; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public partial class MovementPathContainer : Path { private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); + private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); public MovementPathContainer() { @@ -20,7 +21,12 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis lifetimeManager.EntryBecameDead += entryBecameDead; PathRadius = 1f; - Colour = new Color4(255, 255, 255, 127); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Pink2; } protected override void Update() @@ -30,17 +36,17 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis lifetimeManager.Update(Time.Current); } - public void Add(AimPointEntry entry) => lifetimeManager.AddEntry(entry); + public void Add(AnalysisFrameEntry entry) => lifetimeManager.AddEntry(entry); private void entryBecameAlive(LifetimeEntry entry) { - aliveEntries.Add((AimPointEntry)entry); + aliveEntries.Add((AnalysisFrameEntry)entry); updateVertices(); } private void entryBecameDead(LifetimeEntry entry) { - aliveEntries.Remove((AimPointEntry)entry); + aliveEntries.Remove((AnalysisFrameEntry)entry); updateVertices(); } @@ -54,9 +60,9 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis } } - private sealed class AimLinePointComparator : IComparer + private sealed class AimLinePointComparator : IComparer { - public int Compare(AimPointEntry? x, AimPointEntry? y) + public int Compare(AnalysisFrameEntry? x, AnalysisFrameEntry? y) { ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(y); diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index d33d468c7e..a42625a9c4 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -66,8 +66,8 @@ namespace osu.Game.Rulesets.Osu.UI { var osuFrame = (OsuReplayFrame)frame; - MovementMarkers.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); - MovementPath.Add(new AimPointEntry(osuFrame.Time, osuFrame.Position)); + MovementMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); + MovementPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.UI leftHeld = false; else if (!leftHeld && leftButton) { - ClickMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, true)); + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); leftHeld = true; } @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.UI rightHeld = false; else if (!rightHeld && rightButton) { - ClickMarkers.Add(new HitMarkerEntry(osuFrame.Time, osuFrame.Position, false)); + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); rightHeld = true; } } From a6ed719454dc3c0c2784f49d4d8ef627e424e9e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 21:04:59 +0900 Subject: [PATCH 117/308] Visual design pass --- .../UI/ReplayAnalysis/HitMarker.cs | 20 ++++++ .../UI/ReplayAnalysis/HitMarkerClick.cs | 66 ++++--------------- .../UI/ReplayAnalysis/HitMarkerMovement.cs | 40 ++++------- .../ReplayAnalysis/MovementPathContainer.cs | 2 +- .../UI/ReplayAnalysisOverlay.cs | 17 +++-- 5 files changed, 61 insertions(+), 84 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs index e2ecba44fc..4fa992ff15 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis @@ -13,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis Origin = Anchor.Centre; } + [Resolved] + private OsuColour colours { get; set; } = null!; + protected override void OnApply(AnalysisFrameEntry entry) { Position = entry.Position; @@ -22,6 +27,21 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis using (BeginAbsoluteSequence(LifetimeEnd - 200)) this.FadeOut(200); + + switch (entry.Action) + { + case OsuAction.LeftButton: + Colour = colours.BlueLight; + break; + + case OsuAction.RightButton: + Colour = colours.YellowLight; + break; + + default: + Colour = colours.Pink2; + break; + } } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs index 4652ea3416..ba41de7caa 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs @@ -1,9 +1,8 @@ -using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { @@ -11,62 +10,25 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public HitMarkerClick() { - const float length = 20; - const float border_size = 3; - InternalChildren = new Drawable[] { - new Box + new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(border_size, length + border_size), - Colour = Colour4.Black.Opacity(0.5F) + Size = new Vector2(15), + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0, + }, }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(border_size, length + border_size), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - } }; } - - [Resolved] - private OsuColour colours { get; set; } = null!; - - protected override void OnApply(AnalysisFrameEntry entry) - { - base.OnApply(entry); - - switch (entry.Action) - { - case OsuAction.LeftButton: - Colour = colours.BlueLight; - break; - - case OsuAction.RightButton: - Colour = colours.YellowLight; - break; - - default: - throw new ArgumentOutOfRangeException(); - } - } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs index 4f9b6f8790..0cda732b39 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs @@ -1,8 +1,7 @@ -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { @@ -10,41 +9,30 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public HitMarkerMovement() { - const float length = 5; - - Colour = Color4.Gray.Opacity(0.4f); - InternalChildren = new Drawable[] { - new Box + new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(3, length), - Colour = Colour4.Black.Opacity(0.5F) + Colour = OsuColour.Gray(0.2f), + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1.2f) }, - new Box + new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(3, length), - Rotation = 90, - Colour = Colour4.Black.Opacity(0.5F) + RelativeSizeAxes = Axes.Both, }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(1, length), - Rotation = 90, - } }; } + + protected override void OnApply(AnalysisFrameEntry entry) + { + base.OnApply(entry); + + Size = new Vector2(entry.Action != null ? 4 : 3); + } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index 1d52157036..ff662c4dfa 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; - PathRadius = 1f; + PathRadius = 0.5f; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index a42625a9c4..682842c56f 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -62,13 +62,12 @@ namespace osu.Game.Rulesets.Osu.UI bool leftHeld = false; bool rightHeld = false; + OsuAction? lastAction = null; + foreach (var frame in replay.Frames) { var osuFrame = (OsuReplayFrame)frame; - MovementMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); - MovementPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); - bool leftButton = osuFrame.Actions.Contains(OsuAction.LeftButton); bool rightButton = osuFrame.Actions.Contains(OsuAction.RightButton); @@ -76,17 +75,25 @@ namespace osu.Game.Rulesets.Osu.UI leftHeld = false; else if (!leftHeld && leftButton) { - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); leftHeld = true; + lastAction = OsuAction.LeftButton; + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); } if (rightHeld && !rightButton) rightHeld = false; else if (!rightHeld && rightButton) { - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); rightHeld = true; + lastAction = OsuAction.RightButton; + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); } + + if (!leftButton && !rightButton) + lastAction = null; + + MovementMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, lastAction)); + MovementPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); } } } From 21146c35015b2aa6bbd9e015b97313061090c75b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Sep 2024 21:16:59 +0900 Subject: [PATCH 118/308] Add back shadow cast --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 09dcd54c3c..c6ad10c062 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.UI public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; + protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; + public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList? mods = null) : base(ruleset, beatmap, mods) { @@ -43,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.UI if (replayPlayer != null) { PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); - replayPlayer.AddSettings(new ReplayAnalysisSettings((OsuRulesetConfigManager)Config)); + replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); - cursorHideEnabled = ((OsuRulesetConfigManager)Config).GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); + cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); } } From 08ebc83a89d25ad869ef372e4735ebfe0dcaefaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 12:07:16 +0900 Subject: [PATCH 119/308] Fix path getting misaligned with negative position values --- .../UI/ReplayAnalysis/MovementPathContainer.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index ff662c4dfa..dbe87b7ea7 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; using osu.Game.Graphics; +using osuTK; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { @@ -54,10 +55,19 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { ClearVertices(); + Vector2 min = Vector2.Zero; + foreach (var entry in aliveEntries) { AddVertex(entry.Position); + if (entry.Position.X < min.X) + min.X = entry.Position.X; + + if (entry.Position.Y < min.Y) + min.Y = entry.Position.Y; } + + Position = min; } private sealed class AimLinePointComparator : IComparer From ee26ff2e2901bbb51cf477b6c1c3289dcc19b9f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 13:07:49 +0900 Subject: [PATCH 120/308] Add out-of-bounds tests to test case --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index fb6d59ddba..b2cb8c5468 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -94,8 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests for (int i = 0; i < 1000; i++) { - posX = Math.Clamp(posX + random.Next(-20, 21), 0, 500); - posY = Math.Clamp(posY + random.Next(-20, 21), 0, 500); + posX = Math.Clamp(posX + random.Next(-20, 21), -100, 600); + posY = Math.Clamp(posY + random.Next(-20, 21), -100, 600); var actions = new List(); From 2d198e57e1ee2c7f7e6f71009c384301c650317c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 14:02:05 +0900 Subject: [PATCH 121/308] Second visual design pass --- .../UI/ReplayAnalysis/HitMarker.cs | 32 ++--------- .../UI/ReplayAnalysis/HitMarkerClick.cs | 57 ++++++++++++++++++- .../UI/ReplayAnalysis/HitMarkerMovement.cs | 38 ++++++++++--- .../ReplayAnalysis/MovementPathContainer.cs | 2 + .../UI/ReplayAnalysisOverlay.cs | 2 +- 5 files changed, 93 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs index 4fa992ff15..2187fdfe57 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs @@ -8,40 +8,20 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarker : PoolableDrawableWithLifetime + public abstract partial class HitMarker : PoolableDrawableWithLifetime { - public HitMarker() + [Resolved] + protected OsuColour Colours { get; private set; } = null!; + + [BackgroundDependencyLoader] + private void load() { Origin = Anchor.Centre; } - [Resolved] - private OsuColour colours { get; set; } = null!; - protected override void OnApply(AnalysisFrameEntry entry) { Position = entry.Position; - - using (BeginAbsoluteSequence(LifetimeStart)) - Show(); - - using (BeginAbsoluteSequence(LifetimeEnd - 200)) - this.FadeOut(200); - - switch (entry.Action) - { - case OsuAction.LeftButton: - Colour = colours.BlueLight; - break; - - case OsuAction.RightButton: - Colour = colours.YellowLight; - break; - - default: - Colour = colours.Pink2; - break; - } } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs index ba41de7caa..a1024d1930 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs @@ -1,3 +1,4 @@ +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -8,17 +9,30 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public partial class HitMarkerClick : HitMarker { - public HitMarkerClick() + private Container clickDisplay = null!; + + [BackgroundDependencyLoader] + private void load() { InternalChildren = new Drawable[] { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.125f), + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Colour = Colours.Gray5, + }, new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(15), + RelativeSizeAxes = Axes.Both, + Colour = Colours.Gray5, Masking = true, - BorderThickness = 2, + BorderThickness = 2.2f, BorderColour = Color4.White, Child = new Box { @@ -28,7 +42,44 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis Alpha = 0, }, }, + clickDisplay = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Colour = Colours.Yellow, + Scale = new Vector2(0.95f), + Width = 0.5f, + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Width = 2, + Masking = true, + BorderThickness = 2, + BorderColour = Color4.White, + Child = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0, + }, + } + } }; + + Size = new Vector2(16); + } + + protected override void OnApply(AnalysisFrameEntry entry) + { + base.OnApply(entry); + + clickDisplay.Alpha = entry.Action != null ? 1 : 0; + clickDisplay.Rotation = entry.Action == OsuAction.LeftButton ? 0 : 180; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs index 0cda732b39..b2bbb47c4b 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs @@ -1,29 +1,47 @@ +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public partial class HitMarkerMovement : HitMarker { - public HitMarkerMovement() + private Container clickDisplay = null!; + private Circle mainCircle = null!; + + [BackgroundDependencyLoader] + private void load() { InternalChildren = new Drawable[] { - new Circle + mainCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Colour = OsuColour.Gray(0.2f), RelativeSizeAxes = Axes.Both, - Size = new Vector2(1.2f) + Colour = Colours.Pink2, }, - new Circle + clickDisplay = new Container { + Colour = Colours.Yellow, + Scale = new Vector2(0.8f), Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, + Width = 0.5f, + Masking = true, + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Width = 2, + Colour = Color4.White, + }, + }, }, }; } @@ -31,8 +49,12 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis protected override void OnApply(AnalysisFrameEntry entry) { base.OnApply(entry); + Size = new Vector2(entry.Action != null ? 4 : 2.5f); - Size = new Vector2(entry.Action != null ? 4 : 3); + mainCircle.Colour = entry.Action != null ? Colours.Gray4 : Colours.Pink2; + + clickDisplay.Alpha = entry.Action != null ? 1 : 0; + clickDisplay.Rotation = entry.Action == OsuAction.LeftButton ? 0 : 180; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs index dbe87b7ea7..2ffc6ffe4a 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Performance; using osu.Game.Graphics; @@ -28,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis private void load(OsuColour colours) { Colour = colours.Pink2; + BackgroundColour = colours.Pink2.Opacity(0); } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 682842c56f..06a1930fa3 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -32,8 +32,8 @@ namespace osu.Game.Rulesets.Osu.UI InternalChildren = new Drawable[] { - ClickMarkers = new ClickMarkerContainer(), MovementPath = new MovementPathContainer(), + ClickMarkers = new ClickMarkerContainer(), MovementMarkers = new MovementMarkerContainer(), }; } From 4f719b9fec4973a62514c6c8a5619dbae3350203 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 14:28:20 +0900 Subject: [PATCH 122/308] One more rename pass --- .../TestSceneOsuAnalysisContainer.cs | 4 ++-- .../{HitMarker.cs => AnalysisMarker.cs} | 2 +- .../{HitMarkerClick.cs => ClickMarker.cs} | 5 ++++- .../UI/ReplayAnalysis/ClickMarkerContainer.cs | 8 ++++---- ...athContainer.cs => CursorPathContainer.cs} | 4 ++-- .../{HitMarkerMovement.cs => FrameMarker.cs} | 5 ++++- .../UI/ReplayAnalysis/FrameMarkerContainer.cs | 20 +++++++++++++++++++ .../ReplayAnalysis/MovementMarkerContainer.cs | 20 ------------------- .../UI/ReplayAnalysisOverlay.cs | 16 +++++++-------- 9 files changed, 45 insertions(+), 39 deletions(-) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarker.cs => AnalysisMarker.cs} (87%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarkerClick.cs => ClickMarker.cs} (92%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{MovementPathContainer.cs => CursorPathContainer.cs} (96%) rename osu.Game.Rulesets.Osu/UI/ReplayAnalysis/{HitMarkerMovement.cs => FrameMarker.cs} (91%) create mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarkerContainer.cs delete mode 100644 osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index b2cb8c5468..bea4f430ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -124,8 +124,8 @@ namespace osu.Game.Rulesets.Osu.Tests } public bool HitMarkersVisible => ClickMarkers.Alpha > 0 && ClickMarkers.Entries.Any(); - public bool AimMarkersVisible => MovementMarkers.Alpha > 0 && MovementMarkers.Entries.Any(); - public bool AimLinesVisible => MovementPath.Alpha > 0 && MovementPath.Vertices.Count > 1; + public bool AimMarkersVisible => FrameMarkers.Alpha > 0 && FrameMarkers.Entries.Any(); + public bool AimLinesVisible => CursorPath.Alpha > 0 && CursorPath.Vertices.Count > 1; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs similarity index 87% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs index 2187fdfe57..9b602c88a8 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public abstract partial class HitMarker : PoolableDrawableWithLifetime + public abstract partial class AnalysisMarker : PoolableDrawableWithLifetime { [Resolved] protected OsuColour Colours { get; private set; } = null!; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs similarity index 92% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs index a1024d1930..5386a80d9d 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerClick.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs @@ -7,7 +7,10 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarkerClick : HitMarker + /// + /// A marker which shows one click, with visuals focusing on the button which was clicked and the precise location of the click. + /// + public partial class ClickMarker : AnalysisMarker { private Container clickDisplay = null!; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs index 3de1a70d7c..ff94449521 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarkerContainer.cs @@ -6,15 +6,15 @@ using osu.Game.Rulesets.Objects.Pooling; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer + public partial class ClickMarkerContainer : PooledDrawableWithLifetimeContainer { - private readonly DrawablePool clickMarkerPool; + private readonly DrawablePool clickMarkerPool; public ClickMarkerContainer() { - AddInternal(clickMarkerPool = new DrawablePool(30)); + AddInternal(clickMarkerPool = new DrawablePool(30)); } - protected override HitMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry)); + protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => clickMarkerPool.Get(d => d.Apply(entry)); } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs similarity index 96% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs index 2ffc6ffe4a..1951d467e2 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementPathContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/CursorPathContainer.cs @@ -12,12 +12,12 @@ using osuTK; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class MovementPathContainer : Path + public partial class CursorPathContainer : Path { private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); private readonly SortedSet aliveEntries = new SortedSet(new AimLinePointComparator()); - public MovementPathContainer() + public CursorPathContainer() { lifetimeManager.EntryBecameAlive += entryBecameAlive; lifetimeManager.EntryBecameDead += entryBecameDead; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs similarity index 91% rename from osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs rename to osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs index b2bbb47c4b..6d44307075 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/HitMarkerMovement.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs @@ -7,7 +7,10 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { - public partial class HitMarkerMovement : HitMarker + /// + /// A marker which shows one movement frame, include any buttons which are pressed. + /// + public partial class FrameMarker : AnalysisMarker { private Container clickDisplay = null!; private Circle mainCircle = null!; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarkerContainer.cs new file mode 100644 index 0000000000..63aea259f7 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarkerContainer.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Pooling; + +namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis +{ + public partial class FrameMarkerContainer : PooledDrawableWithLifetimeContainer + { + private readonly DrawablePool pool; + + public FrameMarkerContainer() + { + AddInternal(pool = new DrawablePool(80)); + } + + protected override AnalysisMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry)); + } +} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs deleted file mode 100644 index d52f54650d..0000000000 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/MovementMarkerContainer.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Pooling; -using osu.Game.Rulesets.Objects.Pooling; - -namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis -{ - public partial class MovementMarkerContainer : PooledDrawableWithLifetimeContainer - { - private readonly DrawablePool pool; - - public MovementMarkerContainer() - { - AddInternal(pool = new DrawablePool(80)); - } - - protected override HitMarker GetDrawable(AnalysisFrameEntry entry) => pool.Get(d => d.Apply(entry)); - } -} diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 06a1930fa3..6d52e3d975 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Osu.UI private BindableBool aimLinesEnabled { get; } = new BindableBool(); protected readonly ClickMarkerContainer ClickMarkers; - protected readonly MovementMarkerContainer MovementMarkers; - protected readonly MovementPathContainer MovementPath; + protected readonly FrameMarkerContainer FrameMarkers; + protected readonly CursorPathContainer CursorPath; private readonly Replay replay; @@ -32,9 +32,9 @@ namespace osu.Game.Rulesets.Osu.UI InternalChildren = new Drawable[] { - MovementPath = new MovementPathContainer(), + CursorPath = new CursorPathContainer(), ClickMarkers = new ClickMarkerContainer(), - MovementMarkers = new MovementMarkerContainer(), + FrameMarkers = new FrameMarkerContainer(), }; } @@ -53,8 +53,8 @@ namespace osu.Game.Rulesets.Osu.UI base.LoadComplete(); hitMarkersEnabled.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimMarkersEnabled.BindValueChanged(enabled => MovementMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimLinesEnabled.BindValueChanged(enabled => MovementPath.FadeTo(enabled.NewValue ? 1 : 0), true); + aimMarkersEnabled.BindValueChanged(enabled => FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + aimLinesEnabled.BindValueChanged(enabled => CursorPath.FadeTo(enabled.NewValue ? 1 : 0), true); } private void loadReplay() @@ -92,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.UI if (!leftButton && !rightButton) lastAction = null; - MovementMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, lastAction)); - MovementPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); + FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, lastAction)); + CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); } } } From 7390d89c75f45bc86b7e638ed3eddbae3d829ddc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:12:02 +0900 Subject: [PATCH 123/308] Switch to using `CircularProgress` for more consistent sizing Also show both mouse buttons at once on frame markers. --- .../UI/ReplayAnalysis/AnalysisFrameEntry.cs | 4 +- .../UI/ReplayAnalysis/ClickMarker.cs | 56 +++++++++---------- .../UI/ReplayAnalysis/FrameMarker.cs | 48 +++++++++------- .../UI/ReplayAnalysisOverlay.cs | 9 +-- 4 files changed, 58 insertions(+), 59 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs index ca11e6aff1..d44def1b67 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs @@ -8,11 +8,11 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { public partial class AnalysisFrameEntry : LifetimeEntry { - public OsuAction? Action { get; } + public OsuAction[] Action { get; } public Vector2 Position { get; } - public AnalysisFrameEntry(double time, Vector2 position, OsuAction? action = null) + public AnalysisFrameEntry(double time, Vector2 position, params OsuAction[] action) { LifetimeStart = time; LifetimeEnd = time + 1_000; diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs index 5386a80d9d..9788ea1aa9 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/ClickMarker.cs @@ -1,7 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -12,7 +17,8 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis /// public partial class ClickMarker : AnalysisMarker { - private Container clickDisplay = null!; + private CircularProgress leftClickDisplay = null!; + private CircularProgress rightClickDisplay = null!; [BackgroundDependencyLoader] private void load() @@ -45,33 +51,27 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis Alpha = 0, }, }, - clickDisplay = new Container + leftClickDisplay = new CircularProgress { - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, Colour = Colours.Yellow, - Scale = new Vector2(0.95f), - Width = 0.5f, - Masking = true, - BorderThickness = 2, - BorderColour = Color4.White, - Child = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Width = 2, - Masking = true, - BorderThickness = 2, - BorderColour = Color4.White, - Child = new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0, - }, - } - } + Size = new Vector2(0.95f), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Rotation = 180, + Progress = 0.5f, + InnerRadius = 0.18f, + RelativeSizeAxes = Axes.Both, + }, + rightClickDisplay = new CircularProgress + { + Colour = Colours.Yellow, + Size = new Vector2(0.95f), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Progress = 0.5f, + InnerRadius = 0.18f, + RelativeSizeAxes = Axes.Both, + }, }; Size = new Vector2(16); @@ -81,8 +81,8 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { base.OnApply(entry); - clickDisplay.Alpha = entry.Action != null ? 1 : 0; - clickDisplay.Rotation = entry.Action == OsuAction.LeftButton ? 0 : 180; + leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0; + rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs index 6d44307075..35ee144568 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/FrameMarker.cs @@ -1,9 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis { @@ -12,7 +15,8 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis /// public partial class FrameMarker : AnalysisMarker { - private Container clickDisplay = null!; + private CircularProgress leftClickDisplay = null!; + private CircularProgress rightClickDisplay = null!; private Circle mainCircle = null!; [BackgroundDependencyLoader] @@ -27,24 +31,26 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis RelativeSizeAxes = Axes.Both, Colour = Colours.Pink2, }, - clickDisplay = new Container + leftClickDisplay = new CircularProgress { Colour = Colours.Yellow, - Scale = new Vector2(0.8f), - Anchor = Anchor.Centre, + Size = new Vector2(0.8f), + Anchor = Anchor.CentreLeft, Origin = Anchor.CentreRight, + Rotation = 180, + Progress = 0.5f, + InnerRadius = 0.5f, + RelativeSizeAxes = Axes.Both, + }, + rightClickDisplay = new CircularProgress + { + Colour = Colours.Yellow, + Size = new Vector2(0.8f), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Progress = 0.5f, + InnerRadius = 0.5f, RelativeSizeAxes = Axes.Both, - Width = 0.5f, - Masking = true, - Children = new Drawable[] - { - new Circle - { - RelativeSizeAxes = Axes.Both, - Width = 2, - Colour = Color4.White, - }, - }, }, }; } @@ -52,12 +58,12 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis protected override void OnApply(AnalysisFrameEntry entry) { base.OnApply(entry); - Size = new Vector2(entry.Action != null ? 4 : 2.5f); + Size = new Vector2(entry.Action.Any() ? 4 : 2.5f); - mainCircle.Colour = entry.Action != null ? Colours.Gray4 : Colours.Pink2; + mainCircle.Colour = entry.Action.Any() ? Colours.Gray4 : Colours.Pink2; - clickDisplay.Alpha = entry.Action != null ? 1 : 0; - clickDisplay.Rotation = entry.Action == OsuAction.LeftButton ? 0 : 180; + leftClickDisplay.Alpha = entry.Action.Contains(OsuAction.LeftButton) ? 1 : 0; + rightClickDisplay.Alpha = entry.Action.Contains(OsuAction.RightButton) ? 1 : 0; } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 6d52e3d975..eb1ef49e5d 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -62,8 +62,6 @@ namespace osu.Game.Rulesets.Osu.UI bool leftHeld = false; bool rightHeld = false; - OsuAction? lastAction = null; - foreach (var frame in replay.Frames) { var osuFrame = (OsuReplayFrame)frame; @@ -76,7 +74,6 @@ namespace osu.Game.Rulesets.Osu.UI else if (!leftHeld && leftButton) { leftHeld = true; - lastAction = OsuAction.LeftButton; ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); } @@ -85,14 +82,10 @@ namespace osu.Game.Rulesets.Osu.UI else if (!rightHeld && rightButton) { rightHeld = true; - lastAction = OsuAction.RightButton; ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); } - if (!leftButton && !rightButton) - lastAction = null; - - FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, lastAction)); + FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, osuFrame.Actions.ToArray())); CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); } } From 7983a765ab09c47fd0cf908aa6b758420e98f9ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:12:16 +0900 Subject: [PATCH 124/308] Update test scene to show more button holds (including both buttons sometimes) --- .../TestSceneOsuAnalysisContainer.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index bea4f430ea..292ecb7fde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -90,26 +90,28 @@ namespace osu.Game.Rulesets.Osu.Tests var random = new Random(); int posX = 250; int posY = 250; - bool leftOrRight = false; + + var actions = new HashSet(); for (int i = 0; i < 1000; i++) { posX = Math.Clamp(posX + random.Next(-20, 21), -100, 600); posY = Math.Clamp(posY + random.Next(-20, 21), -100, 600); - var actions = new List(); - - if (i % 20 == 0) + if (random.NextDouble() > (actions.Count == 0 ? 0.9 : 0.95)) { - actions.Add(leftOrRight ? OsuAction.LeftButton : OsuAction.RightButton); - leftOrRight = !leftOrRight; + actions.Add(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton); + } + else if (random.NextDouble() > 0.7) + { + actions.Remove(random.NextDouble() > 0.5 ? OsuAction.LeftButton : OsuAction.RightButton); } frames.Add(new OsuReplayFrame { Time = Time.Current + i * 15, Position = new Vector2(posX, posY), - Actions = actions + Actions = actions.ToList(), }); } From 47a9b345ebc52cdecd8e772510014d372f9376c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:15:15 +0900 Subject: [PATCH 125/308] Rename config variables and setting strings --- .../TestSceneOsuAnalysisContainer.cs | 24 +++++++++---------- .../Configuration/OsuRulesetConfigManager.cs | 12 +++++----- .../UI/ReplayAnalysisOverlay.cs | 18 +++++++------- .../UI/ReplayAnalysisSettings.cs | 24 +++++++++---------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index 292ecb7fde..fb8ac81b2c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -40,9 +40,9 @@ namespace osu.Game.Rulesets.Osu.Tests settings = new ReplayAnalysisSettings(config), }; - settings.HitMarkersEnabled.Value = false; - settings.AimMarkersEnabled.Value = false; - settings.AimLinesEnabled.Value = false; + settings.ShowClickMarkers.Value = false; + settings.ShowAimMarkers.Value = false; + settings.ShowCursorPath.Value = false; }); } @@ -51,36 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("enable everything", () => { - settings.HitMarkersEnabled.Value = true; - settings.AimMarkersEnabled.Value = true; - settings.AimLinesEnabled.Value = true; + settings.ShowClickMarkers.Value = true; + settings.ShowAimMarkers.Value = true; + settings.ShowCursorPath.Value = true; }); } [Test] public void TestHitMarkers() { - AddStep("enable hit markers", () => settings.HitMarkersEnabled.Value = true); + AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true); AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); - AddStep("disable hit markers", () => settings.HitMarkersEnabled.Value = false); + AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false); AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); } [Test] public void TestAimMarker() { - AddStep("enable aim markers", () => settings.AimMarkersEnabled.Value = true); + AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true); AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); - AddStep("disable aim markers", () => settings.AimMarkersEnabled.Value = false); + AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false); AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); } [Test] public void TestAimLines() { - AddStep("enable aim lines", () => settings.AimLinesEnabled.Value = true); + AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true); AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); - AddStep("disable aim lines", () => settings.AimLinesEnabled.Value = false); + AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false); AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); } diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index df5cd55c33..e6002523b1 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -23,9 +23,9 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.ShowCursorRipples, false); SetDefault(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); - SetDefault(OsuRulesetSetting.ReplayHitMarkersEnabled, false); - SetDefault(OsuRulesetSetting.ReplayAimMarkersEnabled, false); - SetDefault(OsuRulesetSetting.ReplayAimLinesEnabled, false); + SetDefault(OsuRulesetSetting.ReplayClickMarkersEnabled, false); + SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false); + SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false); } } @@ -39,9 +39,9 @@ namespace osu.Game.Rulesets.Osu.Configuration PlayfieldBorderStyle, // Replay - ReplayHitMarkersEnabled, - ReplayAimMarkersEnabled, - ReplayAimLinesEnabled, + ReplayClickMarkersEnabled, + ReplayFrameMarkersEnabled, + ReplayCursorPathEnabled, ReplayCursorHideEnabled, } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index eb1ef49e5d..622c32c51e 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -14,9 +14,9 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class ReplayAnalysisOverlay : CompositeDrawable { - private BindableBool hitMarkersEnabled { get; } = new BindableBool(); - private BindableBool aimMarkersEnabled { get; } = new BindableBool(); - private BindableBool aimLinesEnabled { get; } = new BindableBool(); + private BindableBool showClickMarkers { get; } = new BindableBool(); + private BindableBool showFrameMarkers { get; } = new BindableBool(); + private BindableBool showCursorPath { get; } = new BindableBool(); protected readonly ClickMarkerContainer ClickMarkers; protected readonly FrameMarkerContainer FrameMarkers; @@ -43,18 +43,18 @@ namespace osu.Game.Rulesets.Osu.UI { loadReplay(); - config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, hitMarkersEnabled); - config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, aimMarkersEnabled); - config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, aimLinesEnabled); + config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, showClickMarkers); + config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, showFrameMarkers); + config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, showCursorPath); } protected override void LoadComplete() { base.LoadComplete(); - hitMarkersEnabled.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimMarkersEnabled.BindValueChanged(enabled => FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - aimLinesEnabled.BindValueChanged(enabled => CursorPath.FadeTo(enabled.NewValue ? 1 : 0), true); + showClickMarkers.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + showFrameMarkers.BindValueChanged(enabled => FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); + showCursorPath.BindValueChanged(enabled => CursorPath.FadeTo(enabled.NewValue ? 1 : 0), true); } private void loadReplay() diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index dd09ee146b..7daab68180 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -13,17 +13,17 @@ namespace osu.Game.Rulesets.Osu.UI { private readonly OsuRulesetConfigManager config; - [SettingSource("Hit markers", SettingControlType = typeof(PlayerCheckbox))] - public BindableBool HitMarkersEnabled { get; } = new BindableBool(); + [SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool ShowClickMarkers { get; } = new BindableBool(); - [SettingSource("Aim markers", SettingControlType = typeof(PlayerCheckbox))] - public BindableBool AimMarkersEnabled { get; } = new BindableBool(); + [SettingSource("Show frame markers", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool ShowAimMarkers { get; } = new BindableBool(); - [SettingSource("Aim lines", SettingControlType = typeof(PlayerCheckbox))] - public BindableBool AimLinesEnabled { get; } = new BindableBool(); + [SettingSource("Show cursor path", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool ShowCursorPath { get; } = new BindableBool(); - [SettingSource("Hide cursor", SettingControlType = typeof(PlayerCheckbox))] - public BindableBool CursorHideEnabled { get; } = new BindableBool(); + [SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))] + public BindableBool HideSkinCursor { get; } = new BindableBool(); public ReplayAnalysisSettings(OsuRulesetConfigManager config) : base("Analysis Settings") @@ -36,10 +36,10 @@ namespace osu.Game.Rulesets.Osu.UI { AddRange(this.CreateSettingsControls()); - config.BindWith(OsuRulesetSetting.ReplayHitMarkersEnabled, HitMarkersEnabled); - config.BindWith(OsuRulesetSetting.ReplayAimMarkersEnabled, AimMarkersEnabled); - config.BindWith(OsuRulesetSetting.ReplayAimLinesEnabled, AimLinesEnabled); - config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, CursorHideEnabled); + config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, ShowClickMarkers); + config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers); + config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath); + config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor); } } } From 0f01a855afd2a4585065eb41f4ee1ab49cdbc0d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:30:42 +0900 Subject: [PATCH 126/308] Add note about cursor hiding being potentially flaky --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index c6ad10c062..16edc654a7 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -48,6 +48,9 @@ namespace osu.Game.Rulesets.Osu.UI replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); + + // I have little faith in this working (other things touch cursor visibility) but haven't broken it yet. + // Let's wait for someone to report an issue before spending too much time on it. cursorHideEnabled.BindValueChanged(enabled => Playfield.Cursor.FadeTo(enabled.NewValue ? 0 : 1), true); } } From a1cf67be629e7f3c4dedaac9aadab6b3f0f73598 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 15:53:53 +0900 Subject: [PATCH 127/308] Add setting to adjust replay analysis display length --- .../Configuration/OsuRulesetConfigManager.cs | 2 + .../UI/ReplayAnalysis/AnalysisFrameEntry.cs | 4 +- .../UI/ReplayAnalysisOverlay.cs | 70 +++++++++++++------ .../UI/ReplayAnalysisSettings.cs | 10 +++ 4 files changed, 64 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index e6002523b1..8a8b78b645 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false); + SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 750); } } @@ -43,5 +44,6 @@ namespace osu.Game.Rulesets.Osu.Configuration ReplayFrameMarkersEnabled, ReplayCursorPathEnabled, ReplayCursorHideEnabled, + ReplayAnalysisDisplayLength, } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs index d44def1b67..116bccc747 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisFrameEntry.cs @@ -12,10 +12,10 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis public Vector2 Position { get; } - public AnalysisFrameEntry(double time, Vector2 position, params OsuAction[] action) + public AnalysisFrameEntry(double time, double displayLength, Vector2 position, params OsuAction[] action) { LifetimeStart = time; - LifetimeEnd = time + 1_000; + LifetimeEnd = time + displayLength; Position = position; Action = action; } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 622c32c51e..0c257a68c5 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -17,10 +17,11 @@ namespace osu.Game.Rulesets.Osu.UI private BindableBool showClickMarkers { get; } = new BindableBool(); private BindableBool showFrameMarkers { get; } = new BindableBool(); private BindableBool showCursorPath { get; } = new BindableBool(); + private BindableInt displayLength { get; } = new BindableInt(); - protected readonly ClickMarkerContainer ClickMarkers; - protected readonly FrameMarkerContainer FrameMarkers; - protected readonly CursorPathContainer CursorPath; + protected ClickMarkerContainer ClickMarkers = null!; + protected FrameMarkerContainer FrameMarkers = null!; + protected CursorPathContainer CursorPath = null!; private readonly Replay replay; @@ -29,36 +30,65 @@ namespace osu.Game.Rulesets.Osu.UI RelativeSizeAxes = Axes.Both; this.replay = replay; - - InternalChildren = new Drawable[] - { - CursorPath = new CursorPathContainer(), - ClickMarkers = new ClickMarkerContainer(), - FrameMarkers = new FrameMarkerContainer(), - }; } + private bool requireDisplay => showClickMarkers.Value || showFrameMarkers.Value || showCursorPath.Value; + [BackgroundDependencyLoader] private void load(OsuRulesetConfigManager config) { - loadReplay(); - config.BindWith(OsuRulesetSetting.ReplayClickMarkersEnabled, showClickMarkers); config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, showFrameMarkers); config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, showCursorPath); + config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, displayLength); } protected override void LoadComplete() { base.LoadComplete(); - showClickMarkers.BindValueChanged(enabled => ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - showFrameMarkers.BindValueChanged(enabled => FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0), true); - showCursorPath.BindValueChanged(enabled => CursorPath.FadeTo(enabled.NewValue ? 1 : 0), true); + showClickMarkers.BindValueChanged(enabled => + { + initialise(); + ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0); + }, true); + showFrameMarkers.BindValueChanged(enabled => + { + initialise(); + FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0); + }, true); + showCursorPath.BindValueChanged(enabled => + { + initialise(); + CursorPath.FadeTo(enabled.NewValue ? 1 : 0); + }, true); + displayLength.BindValueChanged(_ => + { + isLoaded = false; + initialise(); + }, true); } - private void loadReplay() + private bool isLoaded; + + private void initialise() { + if (!requireDisplay) + return; + + if (isLoaded) + return; + + isLoaded = true; + + // It's faster to reinitialise the whole drawable stack than use `Clear` on `PooledDrawableWithLifetimeContainer` + InternalChildren = new Drawable[] + { + CursorPath = new CursorPathContainer(), + ClickMarkers = new ClickMarkerContainer(), + FrameMarkers = new FrameMarkerContainer(), + }; + bool leftHeld = false; bool rightHeld = false; @@ -74,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.UI else if (!leftHeld && leftButton) { leftHeld = true; - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.LeftButton)); + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.LeftButton)); } if (rightHeld && !rightButton) @@ -82,11 +112,11 @@ namespace osu.Game.Rulesets.Osu.UI else if (!rightHeld && rightButton) { rightHeld = true; - ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, OsuAction.RightButton)); + ClickMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, OsuAction.RightButton)); } - FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position, osuFrame.Actions.ToArray())); - CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, osuFrame.Position)); + FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray())); + CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position)); } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index 7daab68180..6acafb5d3b 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -25,6 +25,15 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))] public BindableBool HideSkinCursor { get; } = new BindableBool(); + [SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar))] + public BindableInt DisplayLength { get; } = new BindableInt + { + MinValue = 100, + Default = 800, + MaxValue = 2000, + Precision = 100, + }; + public ReplayAnalysisSettings(OsuRulesetConfigManager config) : base("Analysis Settings") { @@ -40,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.UI config.BindWith(OsuRulesetSetting.ReplayFrameMarkersEnabled, ShowAimMarkers); config.BindWith(OsuRulesetSetting.ReplayCursorPathEnabled, ShowCursorPath); config.BindWith(OsuRulesetSetting.ReplayCursorHideEnabled, HideSkinCursor); + config.BindWith(OsuRulesetSetting.ReplayAnalysisDisplayLength, DisplayLength); } } } From 167e3a337796d925025cba8497300734d6f76a14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 16:01:28 +0900 Subject: [PATCH 128/308] Make loading asynchronous --- .../UI/ReplayAnalysisOverlay.cs | 59 +++++++++++-------- .../UI/ReplayAnalysisSettings.cs | 6 +- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 0c257a68c5..8a48e81111 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -1,8 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Replays; @@ -19,9 +21,9 @@ namespace osu.Game.Rulesets.Osu.UI private BindableBool showCursorPath { get; } = new BindableBool(); private BindableInt displayLength { get; } = new BindableInt(); - protected ClickMarkerContainer ClickMarkers = null!; - protected FrameMarkerContainer FrameMarkers = null!; - protected CursorPathContainer CursorPath = null!; + protected ClickMarkerContainer? ClickMarkers; + protected FrameMarkerContainer? FrameMarkers; + protected CursorPathContainer? CursorPath; private readonly Replay replay; @@ -47,42 +49,43 @@ namespace osu.Game.Rulesets.Osu.UI { base.LoadComplete(); - showClickMarkers.BindValueChanged(enabled => - { - initialise(); - ClickMarkers.FadeTo(enabled.NewValue ? 1 : 0); - }, true); - showFrameMarkers.BindValueChanged(enabled => - { - initialise(); - FrameMarkers.FadeTo(enabled.NewValue ? 1 : 0); - }, true); - showCursorPath.BindValueChanged(enabled => - { - initialise(); - CursorPath.FadeTo(enabled.NewValue ? 1 : 0); - }, true); displayLength.BindValueChanged(_ => { - isLoaded = false; - initialise(); + // Need to fully reload to make this work. + loaded.Invalidate(); }, true); } - private bool isLoaded; + private readonly Cached loaded = new Cached(); + + private CancellationTokenSource? generationCancellationSource; + + protected override void Update() + { + base.Update(); + + if (requireDisplay) + { + initialise(); + + if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0; + if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0; + if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0; + } + } private void initialise() { - if (!requireDisplay) + if (loaded.IsValid) return; - if (isLoaded) - return; + loaded.Validate(); - isLoaded = true; + generationCancellationSource?.Cancel(); + generationCancellationSource = new CancellationTokenSource(); // It's faster to reinitialise the whole drawable stack than use `Clear` on `PooledDrawableWithLifetimeContainer` - InternalChildren = new Drawable[] + var newDrawables = new Drawable[] { CursorPath = new CursorPathContainer(), ClickMarkers = new ClickMarkerContainer(), @@ -92,6 +95,8 @@ namespace osu.Game.Rulesets.Osu.UI bool leftHeld = false; bool rightHeld = false; + // This should probably be async as well, but it's a bit of a pain to debounce and everything. + // Let's address concerns when they are raised. foreach (var frame in replay.Frames) { var osuFrame = (OsuReplayFrame)frame; @@ -118,6 +123,8 @@ namespace osu.Game.Rulesets.Osu.UI FrameMarkers.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position, osuFrame.Actions.ToArray())); CursorPath.Add(new AnalysisFrameEntry(osuFrame.Time, displayLength.Value, osuFrame.Position)); } + + LoadComponentsAsync(newDrawables, drawables => InternalChildrenEnumerable = drawables, generationCancellationSource.Token); } } } diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs index 6acafb5d3b..dc4730d76a 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisSettings.cs @@ -28,10 +28,10 @@ namespace osu.Game.Rulesets.Osu.UI [SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar))] public BindableInt DisplayLength { get; } = new BindableInt { - MinValue = 100, - Default = 800, + MinValue = 200, MaxValue = 2000, - Precision = 100, + Default = 800, + Precision = 200, }; public ReplayAnalysisSettings(OsuRulesetConfigManager config) From 7136483f85461c73d714a7588cb9f8a6a193632d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Sep 2024 09:45:34 +0200 Subject: [PATCH 129/308] Fix nullability inspections --- .../TestSceneOsuAnalysisContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index fb8ac81b2c..d72a347675 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -125,9 +125,9 @@ namespace osu.Game.Rulesets.Osu.Tests { } - public bool HitMarkersVisible => ClickMarkers.Alpha > 0 && ClickMarkers.Entries.Any(); - public bool AimMarkersVisible => FrameMarkers.Alpha > 0 && FrameMarkers.Entries.Any(); - public bool AimLinesVisible => CursorPath.Alpha > 0 && CursorPath.Vertices.Count > 1; + public bool HitMarkersVisible => ClickMarkers?.Alpha > 0 && ClickMarkers.Entries.Any(); + public bool AimMarkersVisible => FrameMarkers?.Alpha > 0 && FrameMarkers.Entries.Any(); + public bool AimLinesVisible => CursorPath?.Alpha > 0 && CursorPath.Vertices.Count > 1; } } } From b9ddac420171e1ecfbcb4374538d8c1c117a92a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Sep 2024 09:45:37 +0200 Subject: [PATCH 130/308] Fix test failures --- .../TestSceneOsuAnalysisContainer.cs | 12 ++++++------ osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs | 8 +++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs index d72a347675..184938ceda 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuAnalysisContainer.cs @@ -61,27 +61,27 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestHitMarkers() { AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true); - AddAssert("hit markers visible", () => analysisContainer.HitMarkersVisible); + AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible); AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false); - AddAssert("hit markers not visible", () => !analysisContainer.HitMarkersVisible); + AddUntilStep("hit markers not visible", () => !analysisContainer.HitMarkersVisible); } [Test] public void TestAimMarker() { AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true); - AddAssert("aim markers visible", () => analysisContainer.AimMarkersVisible); + AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible); AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false); - AddAssert("aim markers not visible", () => !analysisContainer.AimMarkersVisible); + AddUntilStep("aim markers not visible", () => !analysisContainer.AimMarkersVisible); } [Test] public void TestAimLines() { AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true); - AddAssert("aim lines visible", () => analysisContainer.AimLinesVisible); + AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible); AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false); - AddAssert("aim lines not visible", () => !analysisContainer.AimLinesVisible); + AddUntilStep("aim lines not visible", () => !analysisContainer.AimLinesVisible); } private Replay fabricateReplay() diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs index 8a48e81111..2b7f6c9fc9 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysisOverlay.cs @@ -65,13 +65,11 @@ namespace osu.Game.Rulesets.Osu.UI base.Update(); if (requireDisplay) - { initialise(); - if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0; - if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0; - if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0; - } + if (ClickMarkers != null) ClickMarkers.Alpha = showClickMarkers.Value ? 1 : 0; + if (FrameMarkers != null) FrameMarkers.Alpha = showFrameMarkers.Value ? 1 : 0; + if (CursorPath != null) CursorPath.Alpha = showCursorPath.Value ? 1 : 0; } private void initialise() From 86a06c7e103a0a8a4584bdeb0229e5b1631e0869 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 17:19:53 +0900 Subject: [PATCH 131/308] Fix high performance session potentially getting stuck after multiplayer spectator --- osu.Game/Screens/Play/PlayerLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 12048ecbbe..7682bba9a6 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -573,6 +573,9 @@ namespace osu.Game.Screens.Play // if the player never got pushed, we should explicitly dispose it. DisposalTask = LoadTask?.ContinueWith(_ => CurrentPlayer?.Dispose()); } + + highPerformanceSession?.Dispose(); + highPerformanceSession = null; } #endregion From e1b763ff0db4de395f93171aa567cb41e7dd9171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Sep 2024 11:21:59 +0200 Subject: [PATCH 132/308] Apply review suggestions wrt border appearance --- osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 985eb74662..044576c635 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -80,6 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Masking = true; CornerRadius = 5; + CornerExponent = 2.5f; InternalChildren = new Drawable[] { @@ -178,7 +179,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (!disabled) { - BorderThickness = IsHovered || textBox.Focused.Value ? 3 : 0; + BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0; BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; if (textBox.Focused.Value) From 791ce218fcd4c8bff495f869d92d1d25a63d23bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 18:55:11 +0900 Subject: [PATCH 133/308] Add test coverage of beatmap offset edge case failure --- .../Gameplay/TestSceneBeatmapOffsetControl.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs index 3b88750013..c7f1eabab2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -136,6 +137,59 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); } + [Test] + public void TestCalibrationFromNonZeroWithImmediateReferenceScore() + { + const double average_error = -4.5; + const double initial_offset = -2; + + AddStep("Set beatmap offset non-neutral", () => Realm.Write(r => + { + r.Add(new BeatmapInfo + { + ID = Beatmap.Value.BeatmapInfo.ID, + Ruleset = Beatmap.Value.BeatmapInfo.Ruleset, + UserSettings = + { + Offset = initial_offset, + } + }); + })); + + AddStep("Create control with preloaded reference score", () => + { + Child = new PlayerSettingsGroup("Some settings") + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + offsetControl = new BeatmapOffsetControl + { + ReferenceScore = + { + Value = new ScoreInfo + { + HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error), + BeatmapInfo = Beatmap.Value.BeatmapInfo, + } + } + } + } + }; + }); + + AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType().Any()); + AddStep("Press button", () => offsetControl.ChildrenOfType().Single().TriggerClick()); + AddAssert("Offset is adjusted", () => offsetControl.Current.Value, () => Is.EqualTo(initial_offset - average_error)); + + AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType().Single().Enabled.Value); + AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null); + AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any()); + + AddStep("Clean up beatmap", () => Realm.Write(r => r.RemoveAll())); + } + [Test] public void TestCalibrationNoChange() { From 37f61b26ea858b53e29aefc784b1563b8ed56c59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Sep 2024 18:45:49 +0900 Subject: [PATCH 134/308] Fix offset adjust control not correctly applying changes after song select quit This is an interesting scenario where we arrive at a fresh `BeatmapOffsetControl` but with a reference score (from the last play). Our best assumption here is that the beatmap's offset hasn't changed since the last play, so we want to use it for the `lastPlayBeatmapOffset`. But due to unfortunate order of execution, `Current.Value` was not yet initialised. --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 7668d3e635..f312fb0ec5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -104,8 +104,6 @@ namespace osu.Game.Screens.Play.PlayerSettings { base.LoadComplete(); - ReferenceScore.BindValueChanged(scoreChanged, true); - beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, settings => settings.Offset, @@ -124,6 +122,7 @@ namespace osu.Game.Screens.Play.PlayerSettings }); Current.BindValueChanged(currentChanged); + ReferenceScore.BindValueChanged(scoreChanged, true); } private void currentChanged(ValueChangedEvent offset) From e94e08fec3448012c041a301c5d76f1ff0776ee6 Mon Sep 17 00:00:00 2001 From: Sheppsu <49356627+Sheppsu@users.noreply.github.com> Date: Thu, 5 Sep 2024 20:14:36 -0400 Subject: [PATCH 135/308] fix marker depth when rewinding --- osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs index 9b602c88a8..187876d691 100644 --- a/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs +++ b/osu.Game.Rulesets.Osu/UI/ReplayAnalysis/AnalysisMarker.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis protected override void OnApply(AnalysisFrameEntry entry) { Position = entry.Position; + Depth = -(float)entry.LifetimeEnd; } } } From 36a30cf0772d67743032b168886b1f05fd08bc36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Sep 2024 16:01:47 +0900 Subject: [PATCH 136/308] Add note about using hard links in the future --- osu.Game/Database/RealmArchiveModelImporter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 38df2ac1dc..cf0625c51c 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -195,6 +195,7 @@ namespace osu.Game.Database Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + // Consider using hard links here to make this instant. using (var inStream = Files.Storage.GetStream(sourcePath)) using (var outStream = File.Create(destinationPath)) await inStream.CopyToAsync(outStream).ConfigureAwait(false); From 9f834ca1a2db21eea77dd687e8dbdef1ad7932eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Sep 2024 10:24:05 +0200 Subject: [PATCH 137/308] Silence beatmap retrieval failures from results screen favourite button As reported (very poorly) in https://github.com/ppy/osu/pull/28991#issuecomment-2331854970. I believe this is a total edge case and is mostly visible on dev due to some beatmaps existing on `osu.ppy.sh` and not on `dev.ppy.sh`, but I tend to agree in general that these types of failures should not be firing very loud error notifications; logging to network and disabling the button with a tooltip adjustment should be enough. --- osu.Game/Screens/Ranking/FavouriteButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/FavouriteButton.cs b/osu.Game/Screens/Ranking/FavouriteButton.cs index aecaf7c5b9..019b80dde9 100644 --- a/osu.Game/Screens/Ranking/FavouriteButton.cs +++ b/osu.Game/Screens/Ranking/FavouriteButton.cs @@ -76,12 +76,13 @@ namespace osu.Game.Screens.Ranking }; beatmapSetRequest.Failure += e => { - Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}"); + Logger.Log($"Favourite button failed to fetch beatmap info: {e}", LoggingTarget.Network); Schedule(() => { loading.Hide(); Enabled.Value = false; + TooltipText = "this beatmap cannot be favourited"; }); }; api.Queue(beatmapSetRequest); From 6913d75792585bab7f0c649dd6b5687e05753d33 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 6 Sep 2024 18:04:11 +0900 Subject: [PATCH 138/308] Add 'yes'/'no' acronyms to the `played=` filter --- osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs | 4 ++++ osu.Game/Screens/Select/FilterQueryParser.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index e6006b7fd2..9ecfa72947 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -633,11 +633,15 @@ namespace osu.Game.Tests.NonVisual.Filtering new object[] { "0", DateTimeOffset.Now, false }, new object[] { "false", DateTimeOffset.MinValue, true }, new object[] { "false", DateTimeOffset.Now, false }, + new object[] { "no", DateTimeOffset.MinValue, true }, + new object[] { "no", DateTimeOffset.Now, false }, new object[] { "1", DateTimeOffset.MinValue, false }, new object[] { "1", DateTimeOffset.Now, true }, new object[] { "true", DateTimeOffset.MinValue, false }, new object[] { "true", DateTimeOffset.Now, true }, + new object[] { "yes", DateTimeOffset.MinValue, false }, + new object[] { "yes", DateTimeOffset.Now, true }, }; [Test] diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 3e0dba59f0..6c9a95a250 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -159,10 +159,12 @@ namespace osu.Game.Screens.Select switch (value) { case "1": + case "yes": result = true; return true; case "0": + case "no": result = false; return true; From 2c19b7994c70a3b1d0799add0b1018bf9ad7fa6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Sep 2024 11:38:50 +0200 Subject: [PATCH 139/308] Implement "form" check box control --- .../UserInterface/TestSceneFormControls.cs | 16 +- .../Graphics/UserInterfaceV2/FormCheckBox.cs | 155 ++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index f5bc40c869..9c05a34010 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -53,9 +54,22 @@ namespace osu.Game.Tests.Visual.UserInterface PlaceholderText = "Mine is 42!", TabbableContentContainer = this, }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + OnText = "Letterbox", + OffText = "Do not letterbox", + }, + new FormCheckBox + { + Caption = EditorSetupStrings.LetterboxDuringBreaks, + HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, + Current = { Disabled = true }, + }, }, }, - }, + } }; } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs new file mode 100644 index 0000000000..587aa921f5 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -0,0 +1,155 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormCheckBox : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public LocalisableString Caption { get; init; } + public LocalisableString HintText { get; init; } + public LocalisableString OnText { get; init; } = "On"; + public LocalisableString OffText { get; init; } = "Off"; + + private Box background = null!; + private FormFieldCaption caption = null!; + private OsuSpriteText text = null!; + private Nub checkbox = null!; + + private Sample? sampleChecked; + private Sample? sampleUnchecked; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + CornerExponent = 2.5f; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Caption = Caption, + TooltipText = HintText, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + text = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + checkbox = new Nub + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Current = Current, + } + }, + }, + }; + + sampleChecked = audio.Samples.Get(@"UI/check-on"); + sampleUnchecked = audio.Samples.Get(@"UI/check-off"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + current.BindValueChanged(_ => + { + updateState(); + playSamples(); + background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + }); + current.BindDisabledChanged(_ => updateState(), true); + } + + private void playSamples() + { + if (Current.Value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + if (!Current.Disabled) + Current.Value = !Current.Value; + return true; + } + + private void updateState() + { + background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5; + caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + checkbox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + text.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + text.Text = Current.Value ? OnText : OffText; + + if (!Current.Disabled) + { + BorderThickness = IsHovered ? 2 : 0; + + if (IsHovered) + BorderColour = colourProvider.Light4; + } + } + } +} From 15f73a3dfb3dcc838809dd48813c40cfd8d6a784 Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 21:52:42 +1200 Subject: [PATCH 140/308] show participation count in tooltip --- .../Header/Components/DailyChallengeStatsTooltip.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 64a8d67c5b..5d89406c34 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -26,6 +26,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { private StreakPiece currentDaily = null!; private StreakPiece currentWeekly = null!; + private StreakPiece totalParticipation = null!; private StatisticsPiece bestDaily = null!; private StatisticsPiece bestWeekly = null!; private StatisticsPiece topTen = null!; @@ -70,7 +71,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { topBackground = new Box { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.None, }, new FillFlowContainer { @@ -78,8 +79,9 @@ namespace osu.Game.Overlays.Profile.Header.Components Direction = FillDirection.Horizontal, Padding = new MarginPadding(15f), Spacing = new Vector2(30f), - Children = new[] + Children = new Drawable[] { + totalParticipation = new StreakPiece(UsersStrings.ShowDailyChallengePlaycount), currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent), currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent), } @@ -113,6 +115,9 @@ namespace osu.Game.Overlays.Profile.Header.Components background.Colour = colourProvider.Background4; topBackground.Colour = colourProvider.Background5; + totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0")); + totalParticipation.ValueColour = colourProvider.Content2; + currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); From f59895aa3444eca3a167d2c3b315bb054626d091 Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 21:54:41 +1200 Subject: [PATCH 141/308] take out drawable --- .../Profile/Header/Components/DailyChallengeStatsTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 5d89406c34..df52fea158 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -79,7 +79,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Direction = FillDirection.Horizontal, Padding = new MarginPadding(15f), Spacing = new Vector2(30f), - Children = new Drawable[] + Children = new[] { totalParticipation = new StreakPiece(UsersStrings.ShowDailyChallengePlaycount), currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent), From 34a9d60c190c2caf0a20c35508f8e592af6c414f Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 22:02:35 +1200 Subject: [PATCH 142/308] revert back to axes.both --- .../Profile/Header/Components/DailyChallengeStatsTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index df52fea158..93ec3b941a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { topBackground = new Box { - RelativeSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, }, new FillFlowContainer { From ab8771900a44a2a3f01fd5494091866f3ebe7443 Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 22:04:10 +1200 Subject: [PATCH 143/308] change colour --- .../Profile/Header/Components/DailyChallengeStatsTooltip.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index 93ec3b941a..bc389c5569 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Profile.Header.Components topBackground.Colour = colourProvider.Background5; totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0")); - totalParticipation.ValueColour = colourProvider.Content2; + totalParticipation.ValueColour = colours.ForRankingTier(TierForDaily(statistics.PlayCount)); currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); From 362b4bbc566ee0a32391d81f4936ef16a9b8744f Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 23:01:05 +1200 Subject: [PATCH 144/308] Hide daily challenge stats if there are no plays --- .../Profile/Header/Components/DailyChallengeStatsDisplay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 41fd2be591..80487b19c6 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -107,6 +107,12 @@ namespace osu.Game.Overlays.Profile.Header.Components APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics; + if (stats.PlayCount == 0) + { + Hide(); + return; + } + dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); dailyPlayCount.Colour = colours.ForRankingTier(TierForPlayCount(stats.PlayCount)); From a31ea24c6d2ebe7937e2148f6ba754cace417e9d Mon Sep 17 00:00:00 2001 From: Michael Bui Date: Fri, 6 Sep 2024 23:05:04 +1200 Subject: [PATCH 145/308] show stats on all rulesets --- .../Profile/Header/Components/DailyChallengeStatsDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index 80487b19c6..cdc460e1a8 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -99,7 +99,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateDisplay() { - if (User.Value == null || User.Value.Ruleset.OnlineID != 0) + if (User.Value == null) { Hide(); return; From 7e53df5226667d6b7c1ba13bc9898a066e722ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Sep 2024 13:01:50 +0200 Subject: [PATCH 146/308] Add failing test coverage --- .../TestScenePlayerScoreSubmission.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 5e22e47572..c382f0828b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -8,7 +8,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -26,6 +28,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -177,6 +180,30 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("ensure no submission", () => Player.SubmittedScore == null); } + [Test] + public void TestEmptyFailStillImports() + { + prepareTestAPI(true); + + createPlayerTest(true); + + AddUntilStep("wait for token request", () => Player.TokenCreationRequested); + + AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); + AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value, () => Is.EqualTo(Visibility.Visible)); + + AddStep("attempt import", () => + { + InputManager.MoveMouseTo(Player.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("wait for import to start", () => Player.ScoreImportStarted); + AddStep("allow import", () => Player.AllowImportCompletion.Release()); + + AddUntilStep("import completed", () => Player.ImportedScore, () => Is.Not.Null); + AddAssert("ensure no submission", () => Player.SubmittedScore, () => Is.Null); + } + [Test] public void TestSubmissionOnFail() { @@ -378,6 +405,8 @@ namespace osu.Game.Tests.Visual.Gameplay public SemaphoreSlim AllowImportCompletion { get; } public Score ImportedScore { get; private set; } + public new FailOverlay FailOverlay => base.FailOverlay; + public FakeImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) : base(allowPause, showResults, pauseOnFocusLost) { From 4e9ad1388fb0de72c6197faa9ad6d56fb1a87087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 Sep 2024 13:16:27 +0200 Subject: [PATCH 147/308] Fix stall when attempting to import replay after hitting nothing --- osu.Game/Screens/Play/SubmittingPlayer.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 6c5f7fab9e..aea3bf6d5c 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -274,6 +274,16 @@ namespace osu.Game.Screens.Play return Task.CompletedTask; } + // if the user never hit anything, this score should not be counted in any way. + if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) + { + Logger.Log("No hits registered, skipping score submission"); + return Task.CompletedTask; + } + + // mind the timing of this. + // once `scoreSubmissionSource` is created, it is presumed that submission is taking place in the background, + // so all exceptional circumstances that would disallow submission must be handled above. lock (scoreSubmissionLock) { if (scoreSubmissionSource != null) @@ -282,10 +292,6 @@ namespace osu.Game.Screens.Play scoreSubmissionSource = new TaskCompletionSource(); } - // if the user never hit anything, this score should not be counted in any way. - if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) - return Task.CompletedTask; - Logger.Log($"Beginning score submission (token:{token.Value})..."); var request = CreateSubmissionRequest(score, token.Value); From 575da0992fa2f07792368b294cb1430684ea2a44 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Fri, 6 Sep 2024 16:16:40 -0400 Subject: [PATCH 148/308] Fix file associations not updating & uninstalling --- osu.Desktop/Program.cs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 5103663815..5100eef3d9 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -168,12 +168,30 @@ namespace osu.Desktop private static void setupVelopack() { - VelopackApp - .Build() - .WithFirstRun(v => + var app = VelopackApp.Build(); + + app.WithFirstRun(_ => + { + if (OperatingSystem.IsWindows()) + WindowsAssociationManager.InstallAssociations(); + }); + + if (OperatingSystem.IsWindows()) + { + app.WithAfterUpdateFastCallback(_ => { - if (OperatingSystem.IsWindows()) WindowsAssociationManager.InstallAssociations(); - }).Run(); + if (OperatingSystem.IsWindows()) + WindowsAssociationManager.UpdateAssociations(); + }); + + app.WithBeforeUninstallFastCallback(_ => + { + if (OperatingSystem.IsWindows()) + WindowsAssociationManager.UninstallAssociations(); + }); + } + + app.Run(); } } } From ed044d5b85d80b1cdf03555e0be1530ffd166626 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Fri, 6 Sep 2024 22:58:18 +0200 Subject: [PATCH 149/308] Fix proposal for #29736 --- osu.Game/Screens/Ranking/CollectionPopover.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Ranking/CollectionPopover.cs b/osu.Game/Screens/Ranking/CollectionPopover.cs index 6617ac334f..ffc448d7a9 100644 --- a/osu.Game/Screens/Ranking/CollectionPopover.cs +++ b/osu.Game/Screens/Ranking/CollectionPopover.cs @@ -39,6 +39,7 @@ namespace osu.Game.Screens.Ranking new OsuMenu(Direction.Vertical, true) { Items = items, + MaxHeight = 375, }, }; } From 581f190856274ad1c521fb8ae32a503ebeed78ef Mon Sep 17 00:00:00 2001 From: Ianlucht Date: Fri, 6 Sep 2024 16:31:48 -0600 Subject: [PATCH 150/308] fixed issues with search by adding the double quotation marks in the BeatmapSetHeaderContents links. --- osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 168056ea58..d9747d1f44 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Clear(); artist.Clear(); - title.AddLink(titleText, LinkAction.SearchBeatmapSet, titleText); + title.AddLink(titleText, LinkAction.SearchBeatmapSet, "title=\"\"" + titleText + "\"\""); title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); @@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); } - artist.AddLink(artistText, LinkAction.SearchBeatmapSet, artistText); + artist.AddLink(artistText, LinkAction.SearchBeatmapSet, "artist=\"\"" + artistText + "\"\""); if (setInfo.NewValue.TrackId != null) { From 3b81ad4cbffe88ff0ca16a0f26e74fc3c30b7c5b Mon Sep 17 00:00:00 2001 From: Bruno Heredia <111712756+Bruno5430@users.noreply.github.com> Date: Sat, 7 Sep 2024 01:42:47 -0300 Subject: [PATCH 151/308] Fix scroll speed slider defaulting to 0.01 --- osu.Game/Screens/Edit/Timing/EffectSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index a4b9f37dff..f9ef460232 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Timing isRebinding = true; kiai.Current = newEffectPoint.KiaiModeBindable; - scrollSpeedSlider.Current = new BindableDouble + scrollSpeedSlider.Current = new BindableDouble(1) { MinValue = 0.01, MaxValue = 10, From 41d32ab2ca0a79772e1ba8a3e21ba14fe863f30a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 13:54:12 +0900 Subject: [PATCH 152/308] Fix display length not resetting to default because default was wrong Closes https://github.com/ppy/osu/issues/29757. --- osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index 8a8b78b645..580c7e6bd8 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Configuration SetDefault(OsuRulesetSetting.ReplayFrameMarkersEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorPathEnabled, false); SetDefault(OsuRulesetSetting.ReplayCursorHideEnabled, false); - SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 750); + SetDefault(OsuRulesetSetting.ReplayAnalysisDisplayLength, 800); } } From 9b189fd244f10613f616b077d1f83b6e1396cf85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 13:58:17 +0900 Subject: [PATCH 153/308] Fix windows check weirdness --- osu.Desktop/Program.cs | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 5100eef3d9..e78c2ca636 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Runtime.Versioning; using osu.Desktop.LegacyIpc; using osu.Desktop.Windows; using osu.Framework; @@ -170,28 +171,18 @@ namespace osu.Desktop { var app = VelopackApp.Build(); - app.WithFirstRun(_ => - { - if (OperatingSystem.IsWindows()) - WindowsAssociationManager.InstallAssociations(); - }); - if (OperatingSystem.IsWindows()) - { - app.WithAfterUpdateFastCallback(_ => - { - if (OperatingSystem.IsWindows()) - WindowsAssociationManager.UpdateAssociations(); - }); - - app.WithBeforeUninstallFastCallback(_ => - { - if (OperatingSystem.IsWindows()) - WindowsAssociationManager.UninstallAssociations(); - }); - } + configureWindows(app); app.Run(); } + + [SupportedOSPlatform("windows")] + private static void configureWindows(VelopackApp app) + { + app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations()); + app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations()); + app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations()); + } } } From ac6cce5911d78a4321b679c1d93213954865a5a3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Sat, 7 Sep 2024 17:40:33 +0900 Subject: [PATCH 154/308] Refactor to string interpolation --- osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index d9747d1f44..f9e0c6c380 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Clear(); artist.Clear(); - title.AddLink(titleText, LinkAction.SearchBeatmapSet, "title=\"\"" + titleText + "\"\""); + title.AddLink(titleText, LinkAction.SearchBeatmapSet, $@"title=""""{titleText}"""""); title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); @@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); } - artist.AddLink(artistText, LinkAction.SearchBeatmapSet, "artist=\"\"" + artistText + "\"\""); + artist.AddLink(artistText, LinkAction.SearchBeatmapSet, $@"artist=""""{artistText}"""""); if (setInfo.NewValue.TrackId != null) { From 10ef5a6d6dec110525cf59f7c9e14a0d11709c37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 21:46:43 +0900 Subject: [PATCH 155/308] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 5f3dd2f6f4..7b45b9dec4 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 9d9b42a163..1d76deddac 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 3e3ee3757c7688046b77fe4ee947d8ebf7e68dbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 22:13:54 +0900 Subject: [PATCH 156/308] Add failing test case for difficulty splitting --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index ec072a3dd2..fbed577ed2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -520,6 +520,18 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } + [Solo] + [Test] + public void TestDifficultiesSplitOutOnLoad() + { + loadBeatmaps(new List { TestResources.CreateTestBeatmapSetInfo(diff_count) }, () => new FilterCriteria + { + Sort = SortMode.Difficulty, + }); + + checkVisibleItemCount(false, 3); + } + [Test] public void TestAddRemoveDifficultySort() { From 4c6eb895309c33653bf0e2798ec41920e3567c7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 7 Sep 2024 22:05:33 +0900 Subject: [PATCH 157/308] Fix beatmap difficulties not being split out on first load Closes https://github.com/ppy/osu/issues/29728. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a6a6a2f585..2486b26f25 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -137,6 +137,8 @@ namespace osu.Game.Screens.Select private void loadNewRoot() { + beatmapsSplitOut = activeCriteria.SplitOutDifficulties; + // Ensure no changes are made to the list while we are initialising items. // We'll catch up on changes via subscriptions anyway. BeatmapSetInfo[] loadableSets = detachedBeatmapSets!.ToArray(); @@ -726,7 +728,6 @@ namespace osu.Game.Screens.Select if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) { - beatmapsSplitOut = activeCriteria.SplitOutDifficulties; loadNewRoot(); return; } From 32de8e9b2da88e2edbcb06ae8434c15a342dc3fe Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sat, 7 Sep 2024 16:15:00 +0200 Subject: [PATCH 158/308] Fixed ControlPointTable items being blocked by buttons --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 501d8c0e41..c0b9ccb2be 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -53,6 +53,7 @@ namespace osu.Game.Screens.Edit.Timing private void load(OverlayColourProvider colours) { RelativeSizeAxes = Axes.Both; + Padding = new() { Bottom = 50 }; InternalChildren = new Drawable[] { From 2bc6547d49e3578c8bbb5590dafcaf93781eccf5 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sat, 7 Sep 2024 16:23:23 +0200 Subject: [PATCH 159/308] Code quality fix: added type --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index c0b9ccb2be..dd0cf2116e 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Edit.Timing private void load(OverlayColourProvider colours) { RelativeSizeAxes = Axes.Both; - Padding = new() { Bottom = 50 }; + Padding = new MarginPadding { Bottom = 50 }; InternalChildren = new Drawable[] { From 958bfde51d49f526d928a5976a6c0c4f21250bbb Mon Sep 17 00:00:00 2001 From: Ianlucht <90893791+Ianlucht@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:46:42 -0600 Subject: [PATCH 160/308] added DailyChallengeIntro to notification --- .../DailyChallenge/NewDailyChallengeNotification.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index ea19828a21..e305de0aaf 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -5,12 +5,14 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; using osu.Game.Localisation; + namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public partial class NewDailyChallengeNotification : SimpleNotification @@ -24,14 +26,18 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge this.room = room; } + [BackgroundDependencyLoader] - private void load(OsuGame? game) + private void load(OsuGame? game, SessionStatics statics) { Text = DailyChallengeStrings.ChallengeLiveNotification; Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); Activated = () => { - game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); + if(statics.Get(Static.DailyChallengeIntroPlayed)) + game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); + else + game?.PerformFromScreen(s => s.Push(new DailyChallengeIntro(room)), [typeof(MainMenu)]); return true; }; } From 170737b68f76d9269a81b7c91125c7518933f0b2 Mon Sep 17 00:00:00 2001 From: Ianlucht <90893791+Ianlucht@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:48:14 -0600 Subject: [PATCH 161/308] added DailyChallengeIntro to notification --- .../OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index e305de0aaf..8e4337274f 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -12,7 +12,6 @@ using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; using osu.Game.Localisation; - namespace osu.Game.Screens.OnlinePlay.DailyChallenge { public partial class NewDailyChallengeNotification : SimpleNotification @@ -38,6 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); else game?.PerformFromScreen(s => s.Push(new DailyChallengeIntro(room)), [typeof(MainMenu)]); + return true; }; } From e6f81abc3bcaaa82540931d30ed42485165231e8 Mon Sep 17 00:00:00 2001 From: Ianlucht <90893791+Ianlucht@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:57:12 -0600 Subject: [PATCH 162/308] cleaned up whitespace --- .../OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index 8e4337274f..35191f4ffa 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -25,7 +25,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge this.room = room; } - [BackgroundDependencyLoader] private void load(OsuGame? game, SessionStatics statics) { From cd94d6e2bcad42b630930621fa95bb3c54761e40 Mon Sep 17 00:00:00 2001 From: Ianlucht <90893791+Ianlucht@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:01:38 -0600 Subject: [PATCH 163/308] fixed if statement format --- .../OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs index 35191f4ffa..7ae6992bec 100644 --- a/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs +++ b/osu.Game/Screens/OnlinePlay/DailyChallenge/NewDailyChallengeNotification.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!)); Activated = () => { - if(statics.Get(Static.DailyChallengeIntroPlayed)) + if (statics.Get(Static.DailyChallengeIntroPlayed)) game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]); else game?.PerformFromScreen(s => s.Push(new DailyChallengeIntro(room)), [typeof(MainMenu)]); From 4cf057db8f62b82ffac6d954670e3bcbe0711e72 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Sun, 8 Sep 2024 01:13:48 -0400 Subject: [PATCH 164/308] Completely disable velopack when using external update manager --- osu.Desktop/Program.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 5103663815..117ba66784 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -168,6 +168,14 @@ namespace osu.Desktop private static void setupVelopack() { + string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); + + if (!string.IsNullOrEmpty(packageManaged)) + { + Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup"); + return; + } + VelopackApp .Build() .WithFirstRun(v => From 7f814d3106b67158c16a336de7e01910dee4ba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 14:16:37 +0200 Subject: [PATCH 165/308] Fix incorrect tiers being used for tooltip total participation display Compare: https://github.com/ppy/osu-web/pull/11457/commits/95e4561a54353016f25c3fc859b176038b82088a --- .../Online/TestSceneUserProfileDailyChallenge.cs | 4 ++-- .../Header/Components/DailyChallengeStatsDisplay.cs | 9 +-------- .../Header/Components/DailyChallengeStatsTooltip.cs | 11 +++++++++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs index d7f5f65769..9db30380f6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileDailyChallenge.cs @@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayCountRankingTier() { - AddAssert("1 before silver", () => DailyChallengeStatsDisplay.TierForPlayCount(30) == RankingTier.Bronze); - AddAssert("first silver", () => DailyChallengeStatsDisplay.TierForPlayCount(31) == RankingTier.Silver); + AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze); + AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver); } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs index cdc460e1a8..3e86b2268f 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsDisplay.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -14,7 +13,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Scoring; namespace osu.Game.Overlays.Profile.Header.Components { @@ -114,18 +112,13 @@ namespace osu.Game.Overlays.Profile.Header.Components } dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0")); - dailyPlayCount.Colour = colours.ForRankingTier(TierForPlayCount(stats.PlayCount)); + dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount)); TooltipContent = new DailyChallengeTooltipData(colourProvider, stats); Show(); } - // Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. - // This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would - // get truncated to 10 with an integer division and show a lower tier. - public static RankingTier TierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily((int)Math.Ceiling(playCount / 3.0d)); - public ITooltip GetCustomTooltip() => new DailyChallengeStatsTooltip(); } } diff --git a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs index bc389c5569..24e531bd87 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DailyChallengeStatsTooltip.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.LocalisationExtensions; @@ -116,7 +117,7 @@ namespace osu.Game.Overlays.Profile.Header.Components topBackground.Colour = colourProvider.Background5; totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0")); - totalParticipation.ValueColour = colours.ForRankingTier(TierForDaily(statistics.PlayCount)); + totalParticipation.ValueColour = colours.ForRankingTier(TierForPlayCount(statistics.PlayCount)); currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0")); currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent)); @@ -137,7 +138,13 @@ namespace osu.Game.Overlays.Profile.Header.Components topFifty.ValueColour = colourProvider.Content2; } - // reference: https://github.com/ppy/osu-web/blob/8206e0e91eeea80ccf92f0586561346dd40e085e/resources/js/profile-page/daily-challenge.tsx#L13-L43 + // reference: https://github.com/ppy/osu-web/blob/adf1e94754ba9625b85eba795f4a310caf169eec/resources/js/profile-page/daily-challenge.tsx#L13-L47 + + // Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count. + // This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would + // get truncated to 10 with an integer division and show a lower tier. + public static RankingTier TierForPlayCount(int playCount) => TierForDaily((int)Math.Ceiling(playCount / 3.0d)); + public static RankingTier TierForDaily(int daily) { if (daily > 360) From cf23c6668c3281e5644721665e68ec1265e26868 Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sun, 8 Sep 2024 15:59:23 +0200 Subject: [PATCH 166/308] Added background color to hide beatmap background --- .../Screens/Edit/Timing/ControlPointList.cs | 88 +++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 8699c388b3..6a21ff0053 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -7,11 +7,13 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -30,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } = null!; + [Cached] + private OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -43,51 +48,62 @@ namespace osu.Game.Screens.Edit.Timing RelativeSizeAxes = Axes.Both, Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, }, - new FillFlowContainer + new Container { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding(margins), - Spacing = new Vector2(5), + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, Children = new Drawable[] { - new RoundedButton + new Box { - Text = "Select closest to current time", - Action = goToCurrentGroup, - Size = new Vector2(220, 30), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, + Height = 50, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Colour = overlayColourProvider.Background2, }, - } - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding(margins), - Spacing = new Vector2(5), - Children = new Drawable[] - { - deleteButton = new RoundedButton + new FillFlowContainer { - Text = "-", - Size = new Vector2(30, 30), - Action = delete, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - BackgroundColour = colours.Red3, + Anchor = Anchor.BottomLeft, + Padding = new MarginPadding { Left = margins, Bottom = margins }, + Children = new Drawable[] + { + new RoundedButton + { + Text = "Select closest to current time", + Action = goToCurrentGroup, + Size = new Vector2(220, 30), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + } }, - addButton = new RoundedButton + new FillFlowContainer { - Action = addNew, - Size = new Vector2(160, 30), + Direction = FillDirection.Horizontal, Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, + Spacing = new Vector2(5), + Padding = new MarginPadding { Right = margins, Bottom = margins }, + Children = new Drawable[] + { + deleteButton = new RoundedButton + { + Text = "-", + Size = new Vector2(30, 30), + Action = delete, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + BackgroundColour = colours.Red3, + }, + addButton = new RoundedButton + { + Action = addNew, + Size = new Vector2(160, 30), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + } }, } }, From 2e6f17f25399684681c22f5701717037808c97aa Mon Sep 17 00:00:00 2001 From: schiavoanto Date: Sun, 8 Sep 2024 16:04:10 +0200 Subject: [PATCH 167/308] Fixed wrong OverlayColourScheme --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 6a21ff0053..03ad1a631a 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Timing private Bindable selectedGroup { get; set; } = null!; [Cached] - private OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + private OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); [BackgroundDependencyLoader] private void load(OsuColour colours) From 134bcc85b76748fc5e5b4678f53498a853bd7a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 14:53:43 +0200 Subject: [PATCH 168/308] Add failing test case --- .../SongSelect/TestSceneBeatmapCarousel.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index fbed577ed2..97c46a11fc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -520,7 +520,6 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } - [Solo] [Test] public void TestDifficultiesSplitOutOnLoad() { @@ -1132,6 +1131,32 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); } + [Test] + public void TestCarouselRetainsSelectionFromDifficultySort() + { + List manySets = new List(); + + AddStep("Populate beatmap sets", () => + { + manySets.Clear(); + + for (int i = 1; i <= 50; i++) + manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count)); + }); + + loadBeatmaps(manySets); + + BeatmapInfo chosenBeatmap = null!; + AddStep("select given beatmap", () => carousel.SelectBeatmap(chosenBeatmap = manySets[20].Beatmaps[0])); + AddUntilStep("selection changed", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap)); + + AddStep("sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty })); + AddAssert("selection retained", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap)); + + AddStep("sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title })); + AddAssert("selection retained", () => carousel.SelectedBeatmapInfo, () => Is.EqualTo(chosenBeatmap)); + } + [Test] public void TestFilteringByUserStarDifficulty() { From cefbc76490d5b17f5607fd579b86f3c0b89104fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 15:51:59 +0200 Subject: [PATCH 169/308] Fix selection being dropped when changing carousel sort mode from difficulty sort Closes https://github.com/ppy/osu/issues/29738. This "regressed" in https://github.com/ppy/osu/pull/29639, but if I didn't know better, I'd go as far as saying that this looks like a .NET bug, because the fact that PR broke it looks not sane. The TL;DR on this is that before the pull in question, the offending `.Contains()` check that this commit modifies was called on a `List`. The pull changed the collection type to `BeatmapSetInfo[]`. That said, the call is a LINQ call, so all good, right? Not really. First off, the default overload resolution order means that the previous code would call `List.Contains()`, and not `Enumerable.Contains()`. Then again, why would that matter? In both cases `T` is still `BeatmapSetInfo`, right? Well... about that... It is difficult to tell for sure what precisely is happening here, because of what looks like runtime magic. The end *symptom* is that the old code ended up calling `Array.IndexOf()`, and the new code ends up calling... `Array.IndexOf()`. So while yes, `BeatmapSetInfo` implements `IEquatable` and the expectation would be that `Equals()` should be getting called, the type elision to `object` means that we're back to reference equality semantics, because that's what `EqualityComparer.Default` is. A five-minute github search across dotnet/runtime yields this: https://github.com/dotnet/runtime/blob/c4792a228ea36792b90f87ddf7fce2477e827822/src/coreclr/vm/array.cpp#L984-L990 Now again, if I didn't know better, I'd see that "OPTIMIZATION:" comment, see what transpired in this scenario, and call that optimisation invalid, because it changes semantics. But I *probably* know that the dotnet team knows better and am probably just going to take it for what it is, because blame on that code looks to be years old and it's probably not a new behaviour. (I haven't tested empirically if it is.) Instead the fix is just to tell the `.Contains()` method to use the correct comparer. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2486b26f25..525884c413 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Select // We'll catch up on changes via subscriptions anyway. BeatmapSetInfo[] loadableSets = detachedBeatmapSets!.ToArray(); - if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet)) + if (selectedBeatmapSet != null && !loadableSets.Contains(selectedBeatmapSet.BeatmapSet, EqualityComparer.Default)) selectedBeatmapSet = null; var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo; From 10e84d72e566e0f9188985ac1ae1adfd03865e22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Sep 2024 23:07:17 +0900 Subject: [PATCH 170/308] Fix restart notifications appearing every 30 minutes If a user was to manually check for updates via the button, the recheck would have been fired. This is a recent regression. I kinda want to reorganise this code (the button press for check for udpates shouldn't even get close to the recheck code IMO) but for now this seems like one we should quickly fix. Addresses https://github.com/ppy/osu/discussions/29774. --- osu.Desktop/Updater/VelopackUpdateManager.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index e550755fff..c2965428f7 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -45,14 +45,17 @@ namespace osu.Desktop.Updater private async Task checkForUpdateAsync(UpdateProgressNotification? notification = null) { - // should we schedule a retry on completion of this check? - bool scheduleRecheck = true; + // whether to check again in 30 minutes. generally only if there's an error or no update was found (yet). + bool scheduleRecheck = false; try { // Avoid any kind of update checking while gameplay is running. if (localUserInfo?.IsPlaying.Value == true) + { + scheduleRecheck = true; return false; + } // TODO: we should probably be checking if there's a more recent update, rather than shortcutting here. // Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975). @@ -67,17 +70,20 @@ namespace osu.Desktop.Updater return true; } }); + return true; } pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); - // Handle no updates available. + // No update is available. We'll check again later. if (pendingUpdate == null) + { + scheduleRecheck = true; return false; + } - scheduleRecheck = false; - + // An update is found, let's notify the user and start downloading it. if (notification == null) { notification = new UpdateProgressNotification @@ -113,7 +119,6 @@ namespace osu.Desktop.Updater { if (scheduleRecheck) { - // check again in 30 minutes. Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30); } } From f5c5614eef02feb0c816f31ce1d4e9dae163ecdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 16:29:53 +0200 Subject: [PATCH 171/308] Resolve existing colour provider instead of re-caching own one --- osu.Game/Screens/Edit/Timing/ControlPointList.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 03ad1a631a..4cc356012f 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -32,11 +32,8 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } = null!; - [Cached] - private OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.Both; From 7ec2e0e86696eb63b6b1e4995af2263d91d214f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Sep 2024 16:30:09 +0200 Subject: [PATCH 172/308] Refactor layout code to be a bit less haphazard Visually the same, functionally much saner. --- .../Screens/Edit/Timing/ControlPointList.cs | 38 +++++++++++-------- .../Screens/Edit/Timing/ControlPointTable.cs | 7 +++- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointList.cs b/osu.Game/Screens/Edit/Timing/ControlPointList.cs index 4cc356012f..49e5b76dd6 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointList.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointList.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit.Timing { public partial class ControlPointList : CompositeDrawable { + private ControlPointTable table = null!; + private Container controls = null!; private OsuButton deleteButton = null!; private RoundedButton addButton = null!; @@ -40,12 +42,12 @@ namespace osu.Game.Screens.Edit.Timing const float margins = 10; InternalChildren = new Drawable[] { - new ControlPointTable + table = new ControlPointTable { RelativeSizeAxes = Axes.Both, Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, }, }, - new Container + controls = new Container { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -55,15 +57,16 @@ namespace osu.Game.Screens.Edit.Timing { new Box { - Height = 50, - RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Colour = overlayColourProvider.Background2, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2, }, new FillFlowContainer { - Anchor = Anchor.BottomLeft, - Padding = new MarginPadding { Left = margins, Bottom = margins }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Left = margins, Vertical = margins, }, Children = new Drawable[] { new RoundedButton @@ -71,17 +74,19 @@ namespace osu.Game.Screens.Edit.Timing Text = "Select closest to current time", Action = goToCurrentGroup, Size = new Vector2(220, 30), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, } }, new FillFlowContainer { + AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Anchor = Anchor.BottomRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, Spacing = new Vector2(5), - Padding = new MarginPadding { Right = margins, Bottom = margins }, + Padding = new MarginPadding { Right = margins, Vertical = margins, }, Children = new Drawable[] { deleteButton = new RoundedButton @@ -89,16 +94,16 @@ namespace osu.Game.Screens.Edit.Timing Text = "-", Size = new Vector2(30, 30), Action = delete, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, BackgroundColour = colours.Red3, }, addButton = new RoundedButton { Action = addNew, Size = new Vector2(160, 30), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, }, } }, @@ -132,6 +137,7 @@ namespace osu.Game.Screens.Edit.Timing base.Update(); addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time; + table.Padding = new MarginPadding { Bottom = controls.DrawHeight }; } private void goToCurrentGroup() diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index dd0cf2116e..fd812cfe2b 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -28,6 +28,12 @@ namespace osu.Game.Screens.Edit.Timing { public BindableList Groups { get; } = new BindableList(); + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + [Cached] private Bindable activeTimingPoint { get; } = new Bindable(); @@ -53,7 +59,6 @@ namespace osu.Game.Screens.Edit.Timing private void load(OverlayColourProvider colours) { RelativeSizeAxes = Axes.Both; - Padding = new MarginPadding { Bottom = 50 }; InternalChildren = new Drawable[] { From 19e4cc84d58f50d8e10e9c6d3c78133f47047830 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Sep 2024 01:58:09 +0900 Subject: [PATCH 173/308] Also schedule a re-check on download failure --- osu.Desktop/Updater/VelopackUpdateManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index c2965428f7..ae58a8793c 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -105,6 +105,7 @@ namespace osu.Desktop.Updater catch (Exception e) { // In the case of an error, a separate notification will be displayed. + scheduleRecheck = true; notification.FailDownload(); Logger.Error(e, @"update failed!"); } From 4ff72c5331c3f1d4007f08d5defa2ea0131cb94c Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 9 Sep 2024 03:04:16 -0400 Subject: [PATCH 174/308] Add beatmap icon to windows beatmap files --- osu.Desktop/Windows/Icons.cs | 2 ++ .../Windows/WindowsAssociationManager.cs | 4 ++-- osu.Desktop/beatmap.ico | Bin 0 -> 59403 bytes 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 osu.Desktop/beatmap.ico diff --git a/osu.Desktop/Windows/Icons.cs b/osu.Desktop/Windows/Icons.cs index 67915c101a..9d37a21b49 100644 --- a/osu.Desktop/Windows/Icons.cs +++ b/osu.Desktop/Windows/Icons.cs @@ -13,5 +13,7 @@ namespace osu.Desktop.Windows private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!; public static string Lazer => Path.Join(icon_directory, "lazer.ico"); + + public static string Beatmap => Path.Join(icon_directory, "beatmap.ico"); } } diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index b32c01433d..92cffd0987 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -40,8 +40,8 @@ namespace osu.Desktop.Windows private static readonly FileAssociation[] file_associations = { - new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer), - new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer), + new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), + new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer), new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer), }; diff --git a/osu.Desktop/beatmap.ico b/osu.Desktop/beatmap.ico new file mode 100644 index 0000000000000000000000000000000000000000..410ccfd73d6e42edbf3fc6ccc01bc9e00d6cd9a9 GIT binary patch literal 59403 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 DfNejV literal 0 HcmV?d00001 From d451415d5c6b726a10dcedebca6872f63a8273f1 Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 9 Sep 2024 03:28:50 -0400 Subject: [PATCH 175/308] Move environment variable logic to static bool in OsuGameDesktop --- osu.Desktop/OsuGameDesktop.cs | 6 +++--- osu.Desktop/Program.cs | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index c75a3f0a1a..46bd894c07 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -95,11 +95,11 @@ namespace osu.Desktop return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); } + public static bool IsPackageManaged => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER")); + protected override UpdateManager CreateUpdateManager() { - string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); - - if (!string.IsNullOrEmpty(packageManaged)) + if (IsPackageManaged) return new NoActionUpdateManager(); return new VelopackUpdateManager(); diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 78a20e32dc..6e0234f387 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -169,9 +169,7 @@ namespace osu.Desktop private static void setupVelopack() { - string? packageManaged = Environment.GetEnvironmentVariable("OSU_EXTERNAL_UPDATE_PROVIDER"); - - if (!string.IsNullOrEmpty(packageManaged)) + if (OsuGameDesktop.IsPackageManaged) { Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup"); return; From 63b6f36a29e7e1f4d8d19f945155c29c1013c4af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Sep 2024 18:09:28 +0900 Subject: [PATCH 176/308] Add missing `.` --- osu.Desktop/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 6e0234f387..ebc7509af6 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -171,7 +171,7 @@ namespace osu.Desktop { if (OsuGameDesktop.IsPackageManaged) { - Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup"); + Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup."); return; } From 4ada0bf787c1d1a17a14eadaac2f464c66f7227c Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 9 Sep 2024 12:48:15 -0400 Subject: [PATCH 177/308] Differentiate lazer in menus --- osu.Desktop/osu.Desktop.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 3588317b8a..841672b581 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -5,6 +5,7 @@ true A free-to-win rhythm game. Rhythm is just a *click* away! osu! + osu!lazer osu! osu!(lazer) lazer.ico From f716cb4a7cd0b1edfd52afa4d9533ed2337372cf Mon Sep 17 00:00:00 2001 From: smallketchup82 Date: Mon, 9 Sep 2024 16:11:28 -0400 Subject: [PATCH 178/308] Change to using osu!(lazer) --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 841672b581..bf5f26b352 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -5,7 +5,7 @@ true A free-to-win rhythm game. Rhythm is just a *click* away! osu! - osu!lazer + osu!(lazer) osu! osu!(lazer) lazer.ico From d8a745ec045d95fb5a8ca7785185a70b43054baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 11:24:11 +0200 Subject: [PATCH 179/308] Decouple legacy mania combo counter from abstract eldritch entity --- .../Legacy/LegacyManiaComboCounter.cs | 147 +++++++++++++++--- 1 file changed, 125 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs index 07d014b416..889e6326f7 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaComboCounter.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; @@ -12,17 +15,76 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { - public partial class LegacyManiaComboCounter : LegacyComboCounter + public partial class LegacyManiaComboCounter : CompositeDrawable, ISerialisableDrawable { - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - DisplayedCountText.Anchor = Anchor.Centre; - DisplayedCountText.Origin = Anchor.Centre; + public bool UsesFixedAnchor { get; set; } - PopOutCountText.Anchor = Anchor.Centre; - PopOutCountText.Origin = Anchor.Centre; - PopOutCountText.Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red; + public Bindable Current { get; } = new BindableInt { MinValue = 0 }; + + /// + /// Value shown at the current moment. + /// + public virtual int DisplayedCount + { + get => displayedCount; + private set + { + if (displayedCount.Equals(value)) + return; + + displayedCountText.FadeTo(value == 0 ? 0 : 1); + displayedCountText.Text = value.ToString(CultureInfo.InvariantCulture); + counterContainer.Size = displayedCountText.Size; + + displayedCount = value; + } + } + + private int displayedCount; + + private int previousValue; + + private const double fade_out_duration = 100; + private const double rolling_duration = 20; + + private Container counterContainer = null!; + private LegacySpriteText popOutCountText = null!; + private LegacySpriteText displayedCountText = null!; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, ScoreProcessor scoreProcessor) + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new[] + { + counterContainer = new Container + { + AlwaysPresent = true, + Children = new[] + { + popOutCountText = new LegacySpriteText(LegacyFont.Combo) + { + Alpha = 0, + Blending = BlendingParameters.Additive, + BypassAutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red, + }, + displayedCountText = new LegacySpriteText(LegacyFont.Combo) + { + Alpha = 0, + AlwaysPresent = true, + BypassAutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + } + }; + + Current.BindTo(scoreProcessor.Combo); } [Resolved] @@ -34,6 +96,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { base.LoadComplete(); + displayedCountText.Text = popOutCountText.Text = Current.Value.ToString(CultureInfo.InvariantCulture); + + Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); + + counterContainer.Size = displayedCountText.Size; + direction = scrollingInfo.Direction.GetBoundCopy(); direction.BindValueChanged(_ => updateAnchor()); @@ -56,36 +124,71 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1); } - protected override void OnCountIncrement() + private void updateCount(bool rolling) { - base.OnCountIncrement(); + int prev = previousValue; + previousValue = Current.Value; - PopOutCountText.Hide(); - DisplayedCountText.ScaleTo(new Vector2(1f, 1.4f)) + if (!IsLoaded) + return; + + if (!rolling) + { + FinishTransforms(false, nameof(DisplayedCount)); + + if (prev + 1 == Current.Value) + onCountIncrement(); + else + onCountChange(); + } + else + onCountRolling(); + } + + private void onCountIncrement() + { + popOutCountText.Hide(); + + DisplayedCount = Current.Value; + displayedCountText.ScaleTo(new Vector2(1f, 1.4f)) .ScaleTo(new Vector2(1f), 300, Easing.Out) .FadeIn(120); } - protected override void OnCountChange() + private void onCountChange() { - base.OnCountChange(); + popOutCountText.Hide(); - PopOutCountText.Hide(); - DisplayedCountText.ScaleTo(1f); + if (Current.Value == 0) + displayedCountText.FadeOut(); + + DisplayedCount = Current.Value; + + displayedCountText.ScaleTo(1f); } - protected override void OnCountRolling() + private void onCountRolling() { if (DisplayedCount > 0) { - PopOutCountText.Text = FormatCount(DisplayedCount); - PopOutCountText.FadeTo(0.8f).FadeOut(200) + popOutCountText.Text = DisplayedCount.ToString(CultureInfo.InvariantCulture); + popOutCountText.FadeTo(0.8f).FadeOut(200) .ScaleTo(1f).ScaleTo(4f, 200); - DisplayedCountText.FadeTo(0.5f, 300); + displayedCountText.FadeTo(0.5f, 300); } - base.OnCountRolling(); + // Hides displayed count if was increasing from 0 to 1 but didn't finish + if (DisplayedCount == 0 && Current.Value == 0) + displayedCountText.FadeOut(fade_out_duration); + + this.TransformTo(nameof(DisplayedCount), Current.Value, getProportionalDuration(DisplayedCount, Current.Value)); + } + + private double getProportionalDuration(int currentValue, int newValue) + { + double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; + return difference * rolling_duration; } } } From 0e663d18014436c6bcb1d3ba99c6e66f7d299a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 11:27:21 +0200 Subject: [PATCH 180/308] Revert default combo counter code to pre-abstractification (and nuke eldritch abstract entity) --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 8 +- osu.Game/Skinning/LegacyComboCounter.cs | 203 -------------- .../Skinning/LegacyDefaultComboCounter.cs | 264 +++++++++++++++--- 3 files changed, 234 insertions(+), 241 deletions(-) delete mode 100644 osu.Game/Skinning/LegacyComboCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 3a7bc05300..91188f5bac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -440,8 +440,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("import old classic skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"classic-layout-version-0.osk").SkinInfo); AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); - AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType().Any()); - AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); + AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType().Any()); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyDefaultComboCounter { @@ -454,8 +454,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault()); AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin); AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded); - AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType().Count() == 1); - AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); + AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType().Count() == 1); + AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType().Count() == 1); } private Skin importSkinFromArchives(string filename) diff --git a/osu.Game/Skinning/LegacyComboCounter.cs b/osu.Game/Skinning/LegacyComboCounter.cs deleted file mode 100644 index 7003e0d3c8..0000000000 --- a/osu.Game/Skinning/LegacyComboCounter.cs +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Skinning -{ - /// - /// Uses the 'x' symbol and has a pop-out effect while rolling over. - /// - public abstract partial class LegacyComboCounter : CompositeDrawable, ISerialisableDrawable - { - public Bindable Current { get; } = new BindableInt { MinValue = 0 }; - - private const double fade_out_duration = 100; - - /// - /// Duration in milliseconds for the counter roll-up animation for each element. - /// - private const double rolling_duration = 20; - - protected readonly LegacySpriteText PopOutCountText; - protected readonly LegacySpriteText DisplayedCountText; - - private int previousValue; - - private int displayedCount; - - private bool isRolling; - - private readonly Container counterContainer; - - public bool UsesFixedAnchor { get; set; } - - protected LegacyComboCounter() - { - AutoSizeAxes = Axes.Both; - - InternalChildren = new[] - { - counterContainer = new Container - { - AlwaysPresent = true, - Children = new[] - { - PopOutCountText = new LegacySpriteText(LegacyFont.Combo) - { - Alpha = 0, - Blending = BlendingParameters.Additive, - BypassAutoSizeAxes = Axes.Both, - }, - DisplayedCountText = new LegacySpriteText(LegacyFont.Combo) - { - Alpha = 0, - AlwaysPresent = true, - BypassAutoSizeAxes = Axes.Both, - }, - } - } - }; - } - - /// - /// Value shown at the current moment. - /// - public virtual int DisplayedCount - { - get => displayedCount; - private set - { - if (displayedCount.Equals(value)) - return; - - if (isRolling) - onDisplayedCountRolling(value); - else if (displayedCount + 1 == value) - onDisplayedCountIncrement(value); - else - onDisplayedCountChange(value); - - displayedCount = value; - } - } - - [BackgroundDependencyLoader] - private void load(ScoreProcessor scoreProcessor) - { - Current.BindTo(scoreProcessor.Combo); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - DisplayedCountText.Text = FormatCount(Current.Value); - PopOutCountText.Text = FormatCount(Current.Value); - - Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); - - counterContainer.Size = DisplayedCountText.Size; - } - - private void updateCount(bool rolling) - { - int prev = previousValue; - previousValue = Current.Value; - - if (!IsLoaded) - return; - - if (!rolling) - { - FinishTransforms(false, nameof(DisplayedCount)); - - isRolling = false; - DisplayedCount = prev; - - if (prev + 1 == Current.Value) - OnCountIncrement(); - else - OnCountChange(); - } - else - { - OnCountRolling(); - isRolling = true; - } - } - - /// - /// Raised when the counter should display the new value with transitions. - /// - protected virtual void OnCountIncrement() - { - if (DisplayedCount < Current.Value - 1) - DisplayedCount++; - - DisplayedCount++; - } - - /// - /// Raised when the counter should roll to the new combo value (usually roll back to zero). - /// - protected virtual void OnCountRolling() - { - // Hides displayed count if was increasing from 0 to 1 but didn't finish - if (DisplayedCount == 0 && Current.Value == 0) - DisplayedCountText.FadeOut(fade_out_duration); - - transformRoll(DisplayedCount, Current.Value); - } - - /// - /// Raised when the counter should display the new combo value without any transitions. - /// - protected virtual void OnCountChange() - { - if (Current.Value == 0) - DisplayedCountText.FadeOut(); - - DisplayedCount = Current.Value; - } - - private void onDisplayedCountRolling(int newValue) - { - if (newValue == 0) - DisplayedCountText.FadeOut(fade_out_duration); - - DisplayedCountText.Text = FormatCount(newValue); - counterContainer.Size = DisplayedCountText.Size; - } - - private void onDisplayedCountChange(int newValue) - { - DisplayedCountText.FadeTo(newValue == 0 ? 0 : 1); - DisplayedCountText.Text = FormatCount(newValue); - - counterContainer.Size = DisplayedCountText.Size; - } - - private void onDisplayedCountIncrement(int newValue) - { - DisplayedCountText.Text = FormatCount(newValue); - - counterContainer.Size = DisplayedCountText.Size; - } - - private void transformRoll(int currentValue, int newValue) => - this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue)); - - protected virtual string FormatCount(int count) => $@"{count}"; - - private double getProportionalDuration(int currentValue, int newValue) - { - double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; - return difference * rolling_duration; - } - } -} diff --git a/osu.Game/Skinning/LegacyDefaultComboCounter.cs b/osu.Game/Skinning/LegacyDefaultComboCounter.cs index 6c81b1f959..7de4aee656 100644 --- a/osu.Game/Skinning/LegacyDefaultComboCounter.cs +++ b/osu.Game/Skinning/LegacyDefaultComboCounter.cs @@ -1,8 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Threading; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Skinning @@ -10,73 +14,265 @@ namespace osu.Game.Skinning /// /// Uses the 'x' symbol and has a pop-out effect while rolling over. /// - public partial class LegacyDefaultComboCounter : LegacyComboCounter + public partial class LegacyDefaultComboCounter : CompositeDrawable, ISerialisableDrawable { + public Bindable Current { get; } = new BindableInt { MinValue = 0 }; + + private uint scheduledPopOutCurrentId; + private const double big_pop_out_duration = 300; + private const double small_pop_out_duration = 100; - private ScheduledDelegate? scheduledPopOut; + private const double fade_out_duration = 100; + + /// + /// Duration in milliseconds for the counter roll-up animation for each element. + /// + private const double rolling_duration = 20; + + private readonly Drawable popOutCount; + + private readonly Drawable displayedCountSpriteText; + + private int previousValue; + + private int displayedCount; + + private bool isRolling; + + private readonly Container counterContainer; + + public bool UsesFixedAnchor { get; set; } public LegacyDefaultComboCounter() { + AutoSizeAxes = Axes.Both; + + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + Margin = new MarginPadding(10); - PopOutCountText.Anchor = Anchor.BottomLeft; - DisplayedCountText.Anchor = Anchor.BottomLeft; + Scale = new Vector2(1.28f); + + InternalChildren = new[] + { + counterContainer = new Container + { + AlwaysPresent = true, + Children = new[] + { + popOutCount = new LegacySpriteText(LegacyFont.Combo) + { + Alpha = 0, + Blending = BlendingParameters.Additive, + Anchor = Anchor.BottomLeft, + BypassAutoSizeAxes = Axes.Both, + }, + displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo) + { + Alpha = 0, + AlwaysPresent = true, + Anchor = Anchor.BottomLeft, + BypassAutoSizeAxes = Axes.Both, + }, + } + } + }; + } + + /// + /// Value shown at the current moment. + /// + public virtual int DisplayedCount + { + get => displayedCount; + private set + { + if (displayedCount.Equals(value)) + return; + + if (isRolling) + onDisplayedCountRolling(value); + else if (displayedCount + 1 == value) + onDisplayedCountIncrement(value); + else + onDisplayedCountChange(value); + + displayedCount = value; + } + } + + [BackgroundDependencyLoader] + private void load(ScoreProcessor scoreProcessor) + { + Current.BindTo(scoreProcessor.Combo); } protected override void LoadComplete() { base.LoadComplete(); + ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); + ((IHasText)popOutCount).Text = formatCount(Current.Value); + + Current.BindValueChanged(combo => updateCount(combo.NewValue == 0), true); + + updateLayout(); + } + + private void updateLayout() + { const float font_height_ratio = 0.625f; const float vertical_offset = 9; - DisplayedCountText.OriginPosition = new Vector2(0, font_height_ratio * DisplayedCountText.Height + vertical_offset); - DisplayedCountText.Position = new Vector2(0, -(1 - font_height_ratio) * DisplayedCountText.Height + vertical_offset); + displayedCountSpriteText.OriginPosition = new Vector2(0, font_height_ratio * displayedCountSpriteText.Height + vertical_offset); + displayedCountSpriteText.Position = new Vector2(0, -(1 - font_height_ratio) * displayedCountSpriteText.Height + vertical_offset); - PopOutCountText.OriginPosition = new Vector2(3, font_height_ratio * PopOutCountText.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left - PopOutCountText.Position = new Vector2(0, -(1 - font_height_ratio) * PopOutCountText.Height + vertical_offset); + popOutCount.OriginPosition = new Vector2(3, font_height_ratio * popOutCount.Height + vertical_offset); // In stable, the bigger pop out scales a bit to the left + popOutCount.Position = new Vector2(0, -(1 - font_height_ratio) * popOutCount.Height + vertical_offset); + + counterContainer.Size = displayedCountSpriteText.Size; } - protected override void OnCountIncrement() + private void updateCount(bool rolling) { - DisplayedCountText.Show(); + int prev = previousValue; + previousValue = Current.Value; - PopOutCountText.Text = FormatCount(Current.Value); + if (!IsLoaded) + return; - PopOutCountText.ScaleTo(1.56f) - .ScaleTo(1, big_pop_out_duration); - - PopOutCountText.FadeTo(0.6f) - .FadeOut(big_pop_out_duration); - - this.Delay(big_pop_out_duration - 140).Schedule(() => + if (!rolling) { - base.OnCountIncrement(); + FinishTransforms(false, nameof(DisplayedCount)); + isRolling = false; + DisplayedCount = prev; - DisplayedCountText.ScaleTo(1).Then() - .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then() - .ScaleTo(1, small_pop_out_duration / 2, Easing.Out); - }, out scheduledPopOut); + if (prev + 1 == Current.Value) + onCountIncrement(prev, Current.Value); + else + onCountChange(Current.Value); + } + else + { + onCountRolling(displayedCount, Current.Value); + isRolling = true; + } } - protected override void OnCountRolling() + private void transformPopOut(int newValue) { - scheduledPopOut?.Cancel(); - scheduledPopOut = null; + ((IHasText)popOutCount).Text = formatCount(newValue); - base.OnCountRolling(); + popOutCount.ScaleTo(1.56f) + .ScaleTo(1, big_pop_out_duration); + + popOutCount.FadeTo(0.6f) + .FadeOut(big_pop_out_duration); } - protected override void OnCountChange() + private void transformNoPopOut(int newValue) { - scheduledPopOut?.Cancel(); - scheduledPopOut = null; + ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); - base.OnCountChange(); + counterContainer.Size = displayedCountSpriteText.Size; + + displayedCountSpriteText.ScaleTo(1); } - protected override string FormatCount(int count) => $@"{count}x"; + private void transformPopOutSmall(int newValue) + { + ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); + + counterContainer.Size = displayedCountSpriteText.Size; + + displayedCountSpriteText.ScaleTo(1).Then() + .ScaleTo(1.1f, small_pop_out_duration / 2, Easing.In).Then() + .ScaleTo(1, small_pop_out_duration / 2, Easing.Out); + } + + private void scheduledPopOutSmall(uint id) + { + // Too late; scheduled task invalidated + if (id != scheduledPopOutCurrentId) + return; + + DisplayedCount++; + } + + private void onCountIncrement(int currentValue, int newValue) + { + scheduledPopOutCurrentId++; + + if (DisplayedCount < currentValue) + DisplayedCount++; + + displayedCountSpriteText.Show(); + + transformPopOut(newValue); + + uint newTaskId = scheduledPopOutCurrentId; + + Scheduler.AddDelayed(delegate + { + scheduledPopOutSmall(newTaskId); + }, big_pop_out_duration - 140); + } + + private void onCountRolling(int currentValue, int newValue) + { + scheduledPopOutCurrentId++; + + // Hides displayed count if was increasing from 0 to 1 but didn't finish + if (currentValue == 0 && newValue == 0) + displayedCountSpriteText.FadeOut(fade_out_duration); + + transformRoll(currentValue, newValue); + } + + private void onCountChange(int newValue) + { + scheduledPopOutCurrentId++; + + if (newValue == 0) + displayedCountSpriteText.FadeOut(); + + DisplayedCount = newValue; + } + + private void onDisplayedCountRolling(int newValue) + { + if (newValue == 0) + displayedCountSpriteText.FadeOut(fade_out_duration); + else + displayedCountSpriteText.Show(); + + transformNoPopOut(newValue); + } + + private void onDisplayedCountChange(int newValue) + { + displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1); + transformNoPopOut(newValue); + } + + private void onDisplayedCountIncrement(int newValue) + { + displayedCountSpriteText.Show(); + transformPopOutSmall(newValue); + } + + private void transformRoll(int currentValue, int newValue) => + this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue)); + + private string formatCount(int count) => $@"{count}x"; + + private double getProportionalDuration(int currentValue, int newValue) + { + double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; + return difference * rolling_duration; + } } } From b78ef81bf1e5e4be82dac4302936d842f9d9bb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 15:54:07 +0200 Subject: [PATCH 181/308] Fix Flashlight not appearing on top of bubbles from Bubbles mod Inadvertently regressed in 44d0dc6113a408a15a18025325e55646a2147b14. --- osu.Game/Rulesets/Mods/ModFlashlight.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index c924915bd0..64c193d25f 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -83,8 +83,6 @@ namespace osu.Game.Rulesets.Mods flashlight.RelativeSizeAxes = Axes.Both; flashlight.Colour = Color4.Black; - // Flashlight mods should always draw above any other mod adding overlays. - flashlight.Depth = float.MinValue; flashlight.Combo.BindTo(Combo); flashlight.GetPlayfieldScale = () => drawableRuleset.Playfield.Scale; @@ -95,6 +93,9 @@ namespace osu.Game.Rulesets.Mods // workaround for 1px gaps on the edges of the playfield which would sometimes show with "gameplay" screen scaling active. Padding = new MarginPadding(-1), Child = flashlight, + // Flashlight mods should always draw above any other mod adding overlays. + // NegativeInfinity is not used to allow one more thing drawn on top (used in replay analysis overlay in osu!). + Depth = float.MinValue, }); } From 4a39873e2aac3a6fa71a3be06407d9afb7df8922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 15:54:30 +0200 Subject: [PATCH 182/308] Fix replay analysis overlay not rotating with Barrel Roll enabled Closes https://github.com/ppy/osu/issues/29839. --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 3 ++- osu.Game/Rulesets/Mods/ModBarrelRoll.cs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 16edc654a7..4192d678dd 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -44,7 +44,8 @@ namespace osu.Game.Rulesets.Osu.UI { if (replayPlayer != null) { - PlayfieldAdjustmentContainer.Add(new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); + ReplayAnalysisOverlay analysisOverlay; + PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); diff --git a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs index 0c301d293f..4f90496308 100644 --- a/osu.Game/Rulesets/Mods/ModBarrelRoll.cs +++ b/osu.Game/Rulesets/Mods/ModBarrelRoll.cs @@ -40,9 +40,11 @@ namespace osu.Game.Rulesets.Mods public override string SettingDescription => $"{SpinSpeed.Value:N2} rpm {Direction.Value.GetDescription().ToLowerInvariant()}"; + private PlayfieldAdjustmentContainer playfieldAdjustmentContainer = null!; + public void Update(Playfield playfield) { - playfield.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); + playfieldAdjustmentContainer.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) @@ -52,7 +54,9 @@ namespace osu.Game.Rulesets.Mods var playfieldSize = drawableRuleset.Playfield.DrawSize; float minSide = MathF.Min(playfieldSize.X, playfieldSize.Y); float maxSide = MathF.Max(playfieldSize.X, playfieldSize.Y); - drawableRuleset.Playfield.Scale = new Vector2(minSide / maxSide); + + playfieldAdjustmentContainer = drawableRuleset.PlayfieldAdjustmentContainer; + playfieldAdjustmentContainer.Scale = new Vector2(minSide / maxSide); } } } From f38ae5f239ba215268cfc6bbbf4aa2263fc9845b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Sep 2024 15:55:02 +0200 Subject: [PATCH 183/308] Fix replay analysis overlay being affected by visibility impairing mods Closes https://github.com/ppy/osu/issues/29748. --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 4192d678dd..ab69b67051 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -46,6 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI { ReplayAnalysisOverlay analysisOverlay; PlayfieldAdjustmentContainer.Add(analysisOverlay = new ReplayAnalysisOverlay(replayPlayer.Score.Replay)); + Overlays.Add(analysisOverlay.CreateProxy().With(p => p.Depth = float.NegativeInfinity)); replayPlayer.AddSettings(new ReplayAnalysisSettings(Config)); cursorHideEnabled = Config.GetBindable(OsuRulesetSetting.ReplayCursorHideEnabled); From 77c3cb65045876d2443a8c1aff442347eab81cc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Sep 2024 14:38:51 +0900 Subject: [PATCH 184/308] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 7b45b9dec4..d5bdfd91b5 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 1d76deddac..da1cec395f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 652a59061164dadb2db1302d9c896e5cefecdec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 09:59:20 +0200 Subject: [PATCH 185/308] Attempt to address design concerns --- .../UserInterface/TestSceneFormControls.cs | 2 -- .../Graphics/UserInterfaceV2/FormCheckBox.cs | 33 +++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 9c05a34010..be2ba860d3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -58,8 +58,6 @@ namespace osu.Game.Tests.Visual.UserInterface { Caption = EditorSetupStrings.LetterboxDuringBreaks, HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, - OnText = "Letterbox", - OffText = "Do not letterbox", }, new FormCheckBox { diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs index 587aa921f5..6054e898fe 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -14,7 +14,9 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Overlays; +using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -30,8 +32,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 public LocalisableString Caption { get; init; } public LocalisableString HintText { get; init; } - public LocalisableString OnText { get; init; } = "On"; - public LocalisableString OffText { get; init; } = "Off"; private Box background = null!; private FormFieldCaption caption = null!; @@ -74,17 +74,30 @@ namespace osu.Game.Graphics.UserInterfaceV2 Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, }, - text = new OsuSpriteText + new FillFlowContainer { RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - }, - checkbox = new Nub - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Current = Current, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(7), + Children = new Drawable[] + { + checkbox = new Nub + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Current = Current, + Margin = new MarginPadding { Top = 2, }, + }, + text = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } } }, }, @@ -141,7 +154,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 checkbox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; text.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; - text.Text = Current.Value ? OnText : OffText; + text.Text = Current.Value ? CommonStrings.Enabled : CommonStrings.Disabled; if (!Current.Disabled) { From 929ea87975520450ead9e385d9560d105c2f1063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 14:53:59 +0200 Subject: [PATCH 186/308] Revert to checkbox on right --- .../Graphics/UserInterfaceV2/FormCheckBox.cs | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs index 6054e898fe..797ff09800 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -16,7 +16,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Overlays; -using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -74,31 +73,18 @@ namespace osu.Game.Graphics.UserInterfaceV2 Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, }, - new FillFlowContainer + text = new OsuSpriteText { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(7), - Children = new Drawable[] - { - checkbox = new Nub - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = Current, - Margin = new MarginPadding { Top = 2, }, - }, - text = new OsuSpriteText - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - } - } + }, + checkbox = new Nub + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Current = Current, + }, }, }, }; From f71ce8869e8cccd50651f3cbe52355093b0c09e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 14:54:07 +0200 Subject: [PATCH 187/308] Limit width of test scene controls To better reflect what the widths should be in actual usage. --- osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index be2ba860d3..eb8a8b3fe9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -26,7 +26,10 @@ namespace osu.Game.Tests.Visual.UserInterface RelativeSizeAxes = Axes.Both, Child = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, Direction = FillDirection.Vertical, Spacing = new Vector2(5), Padding = new MarginPadding(10), From a4f6d4a300e2da5ba12d271965272f15634b8437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Sep 2024 15:58:41 +0200 Subject: [PATCH 188/308] Backpopulate missing ranked/submitted dates using new local metadata cache People keep asking why https://github.com/ppy/osu/pull/29553 didn't fix their databases (as stated in the PR, it didn't intend to), so this should do it for them. --- .../LocalCachedBeatmapMetadataSource.cs | 17 ++- .../Database/BackgroundDataStoreProcessor.cs | 104 ++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs index 96817571f6..eaa4d8ebfb 100644 --- a/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs +++ b/osu.Game/Beatmaps/LocalCachedBeatmapMetadataSource.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; using Microsoft.Data.Sqlite; @@ -78,7 +79,7 @@ namespace osu.Game.Beatmaps // cached database exists on disk. && storage.Exists(cache_database_name); - public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata) + public bool TryLookup(BeatmapInfo beatmapInfo, [NotNullWhen(true)] out OnlineBeatmapMetadata? onlineMetadata) { Debug.Assert(beatmapInfo.BeatmapSet != null); @@ -98,7 +99,7 @@ namespace osu.Game.Beatmaps try { - using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true)))) + using (var db = getConnection()) { db.Open(); @@ -125,6 +126,9 @@ namespace osu.Game.Beatmaps return false; } + private SqliteConnection getConnection() => + new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))); + private void prepareLocalCache() { bool isRefetch = storage.Exists(cache_database_name); @@ -191,6 +195,15 @@ namespace osu.Game.Beatmaps }); } + public int GetCacheVersion() + { + using (var connection = getConnection()) + { + connection.Open(); + return getCacheVersion(connection); + } + } + private int getCacheVersion(SqliteConnection connection) { using (var cmd = connection.CreateCommand()) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 16ff766ea4..59ef9a3ae1 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Online.API; @@ -61,6 +63,9 @@ namespace osu.Game.Database [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private Storage storage { get; set; } = null!; + protected virtual int TimeToSleepDuringGameplay => 30000; protected override void LoadComplete() @@ -78,6 +83,7 @@ namespace osu.Game.Database processScoresWithMissingStatistics(); convertLegacyTotalScoreToStandardised(); upgradeScoreRanks(); + backpopulateMissingSubmissionAndRankDates(); }, TaskCreationOptions.LongRunning).ContinueWith(t => { if (t.Exception?.InnerException is ObjectDisposedException) @@ -443,6 +449,104 @@ namespace osu.Game.Database completeNotification(notification, processedCount, scoreIds.Count, failedCount); } + private void backpopulateMissingSubmissionAndRankDates() + { + var localMetadataSource = new LocalCachedBeatmapMetadataSource(storage); + + if (!localMetadataSource.Available) + { + Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is missing."); + return; + } + + try + { + if (localMetadataSource.GetCacheVersion() < 2) + { + Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is too old."); + return; + } + } + catch (Exception ex) + { + Logger.Log($"Error when trying to query version of local metadata cache: {ex}"); + return; + } + + Logger.Log("Querying for beatmap sets that contain missing submission/rank date..."); + + HashSet beatmapSetIds = realmAccess.Run(r => new HashSet( + r.All() + .Where(b => b.StatusInt > 0 && (b.DateRanked == null || b.DateSubmitted == null)) + .AsEnumerable() + .Select(b => b.ID))); + + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets with missing submission/rank date."); + + if (beatmapSetIds.Count == 0) + return; + + var notification = showProgressNotification(beatmapSetIds.Count, "Populating missing submission and rank dates", "beatmap sets now have correct submission and rank dates."); + + int processedCount = 0; + int failedCount = 0; + + foreach (var id in beatmapSetIds) + { + if (notification?.State == ProgressNotificationState.Cancelled) + break; + + updateNotificationProgress(notification, processedCount, beatmapSetIds.Count); + + sleepIfRequired(); + + try + { + // Can't use async overload because we're not on the update thread. + // ReSharper disable once MethodHasAsyncOverload + bool succeeded = realmAccess.Write(r => + { + BeatmapSetInfo beatmapSet = r.Find(id)!; + + // we want any ranked representative of the set. + // the reason for checking ranked status of the difficulty is that it can be locally modified, + // at which point the lookup will fail - but there might still be another unmodified difficulty on which it will work. + if (beatmapSet.Beatmaps.FirstOrDefault(b => b.Status >= BeatmapOnlineStatus.Ranked) is not BeatmapInfo beatmap) + return false; + + bool lookupSucceeded = localMetadataSource.TryLookup(beatmap, out var result); + + if (lookupSucceeded) + { + Debug.Assert(result != null); + beatmapSet.DateRanked = result.DateRanked; + beatmapSet.DateSubmitted = result.DateSubmitted; + return true; + } + + Logger.Log($"Could not find {beatmapSet.GetDisplayString()} in local cache while backpopulating missing submission/rank date"); + return false; + }); + + if (succeeded) + ++processedCount; + else + ++failedCount; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception e) + { + Logger.Log($"Failed to update ranked/submitted dates for beatmap set {id}: {e}"); + ++failedCount; + } + } + + completeNotification(notification, processedCount, beatmapSetIds.Count, failedCount); + } + private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount) { if (notification == null) From 562a5006eae0d9f964d7891daa0f203834e0dc3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Sep 2024 02:19:52 +0900 Subject: [PATCH 189/308] Change log output to only output when matches are found (in line with other methods) --- osu.Game/Database/BackgroundDataStoreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs index 59ef9a3ae1..0fa785e494 100644 --- a/osu.Game/Database/BackgroundDataStoreProcessor.cs +++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs @@ -481,11 +481,11 @@ namespace osu.Game.Database .AsEnumerable() .Select(b => b.ID))); - Logger.Log($"Found {beatmapSetIds.Count} beatmap sets with missing submission/rank date."); - if (beatmapSetIds.Count == 0) return; + Logger.Log($"Found {beatmapSetIds.Count} beatmap sets with missing submission/rank date."); + var notification = showProgressNotification(beatmapSetIds.Count, "Populating missing submission and rank dates", "beatmap sets now have correct submission and rank dates."); int processedCount = 0; From 1204136af81596dffdfd7f4a8afc7d31d8788fea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Sep 2024 02:46:19 +0900 Subject: [PATCH 190/308] Update icon file with new design --- osu.Desktop/beatmap.ico | Bin 59403 -> 356968 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/osu.Desktop/beatmap.ico b/osu.Desktop/beatmap.ico index 410ccfd73d6e42edbf3fc6ccc01bc9e00d6cd9a9..58ab198bc273205c90a73d1efbdb16c0751f811f 100644 GIT binary patch literal 356968 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

U-SDh z?s9nC)rY(Z511U69o0c~>(yqun?1Jlnu>j(r+M2|XRvf0Q+u~21J=4;dwa z@TC8X9;EjL-!R!fw9*K=k?g?m|LUu+o?lA#Ct1tG*L(bXSjmpetu3dzdppi`OQx3| zvuYqd_AvZ+pidf3;{4!&>id`f`ek?Z!EdtXFoQYfj?0${%Ume@pFI;^EBxo`|IxaA z-rn}*}Xu5#dhJWy#Vox~5`Jyy)z>L<2#FfsN_Kl@OB>XqyPS%UTe^CT#?0v9Hh=1Rv zd|NayKrK~rQpabcLY5vy$TG9BgGi8Q<_KL3n|3y2QWWOUj&u83> z+NrvR<~D-^?WbJ>PM_tT>$Ak@cQtZ&$>-OtKo@-OW@LA3(FxCAD<=3qfjluGF1jA^ zu8jem;Q^0!o6p;CJ2%JhG@v=vptEP^(VDd*C|7kz+7|fxdKK2ue@AJcg@7h}8lw;8 z8|SCEHqQa~8sly_^4%m%aeKC&;UyzJi|?Rru2~u;h*<;4FB|D!#}vxeT%xJ zN#}Vp$ny>V=z^ql#|!UT*;#MK3p?wn@gJl6Ty(Z@Ki;gp2H8TR%{$~}%zsl-%)C6Gkn(K7X8*b=Dv)s~|`TWD4)>rJ27P3-_`}Z@|D@IUZ#w_*iLT(k zsJ{2w<@En6=eyflM(y`)j$lFCgzFd%9zKP(;`C`Zs}}qV{*h5$-mNjE%b{-@4O|C| z#E0g;w{fTT0V4d*F}hU#VL1{0bLIN-cgBujk)xHoJPKfS(=q759Q3-;x8^)b+l&4` z{;AUaA6?M*IDdbj@Gt(4(H*6BCYbqv?p)4j{)f!R^ge}ut^Zjw=**wqqSC!mTL0Pn z50coAx9>{gzk2aj=J(*Aeq(U6zA`g%2e?YwY2o>>ydA&3#qj_32G_rX`hR}>Bro#6 z`PfOw1l}|l-E{Osch`Ay-Rt+QWZz+n(_8N04;nT-qlK*XHRlL_!vCFTga5cJZ--@l zRCG`@Z^5ddor0JerIz;WL>pO%+VV*uLw4?lbG@rsc zcZ|{Z-;Y9P7M}Oo{Yy*ggNOZJ{2#Lmb3c>+a2G_DI{`AB8A`LCV|RS{{*O*ThH*f5 z{4~He`43};&Xw<18~-=_)BgoG!M#4PC>F18yV*7P|C|{AcNk7e=PejrbMRZh=uP+c zqb5dmAdP79??=C9GL>up@HT6ysrXUPRx{mu&lccc{XfBf>9Xq6zWg||>IIYhH2LM< zc;p1v>9=pVE{D9${^vON@;!@c?f;(tYyL++HvH4~E4Wi$`#%|-^ArE~Kl-`O2kX5I z{|o>3bzBhRpSD5+Jx`jIXA0Y<@HV&PZ)|s?Z*&I#U4GC0nehPD@-3Jj05j1=w?CqT3cpc4y-uE06aR7l_q68QaEI!e zv?sue)g|~gUPk_5yW+HA`xTz#tu($Js#{@LTwk^4+TV|b2HrGY@KQbT4-Reo2d~=C zui)%n7Cmmz%7KsI$C$4@!K}`U!z*;(gRTFe3*FmVEC1K{AL8G)ZHH{y-LHLd{0}&F zj?qe8`Dv`Y_Rx2rfiX_~MR=4gnz-%{%%2IjJSeLe{uhJ)yUv+cds)IyfTOzVv{T`} zjDm7*I!5!HG026-xR>u;Tx7-u3-gpfHL0JN%OW&nsq`V|7A^R@?UVR z{;#`bNs^oY*QfvAaY9Vj)VtS7vlANcrT)fu=vM=Vr0LggPSO5v_&=ULNd6Jeh;b0Y zM{dl`$4qut|L!g00U8I?_pfGL=zi>E%ForWcZvRQ_>bEW)jKXrX~Xh-UiGl^+Fe_G zpTD;Y;%a=o{;+X24}5*(@>=oFTO2n3W8Jrh_&<2nT>!eP*Y1PSTd2%^?Oiq9`_qXx zP_-@q@6r*n^&j(p-6NoLB(?T`&;NS@H;?DqZ1su4@SZ2mE{gwE3%9tJ?pom79) z_mYLyKSd|KT0vjH$!J3O63oa$eZK4Aa&zwiGMo+Ely znws_Xc|~^5n6`cXp3Xeo=h9QhpzTpQ`GtK$<$2@F$~bf6H0P^odFX5^=`J_vmH8gXc}3t-mkLppEn>94MLV;`+{qX$42*FzVV zE-b*=dK^FMI6Cp_QQe{|~#=J;9O7*EcD>#lg-eNWwT z%Pqpc-j=ZWKX|V<|JV3G;G!)Z_p5@xS0MK*Q$J_w zXY)VyhHU>&`@fP4tN(*|)Bk`5O#ZL+U#;W+qOETD#q-=P$4O5Mxn&C(+_I4gnp<|E zv2Y)Ey6e??rkhdAzGjI3Qf^Ndo7j_HwP-W<3$1t4URmefN%+MQ#*qcK&YLu38-&OY85ktWn+x z%)5a*^e^H*=T=y7h!%lRs1I!~^0{=F1Ecz;h98b?+y-eTt{*DTH7&YTae z+;%eOm(J96KeTS$IT8&2yA>)mRu@gnh~ByQ*yO~l_xTu&$X_o+w!_xvBc8~>*d zoBT)TySNiJ&3=xJ|Hy+<^!t?71Uefc{KG43|5xKb%fvd@|GMjA;5qDBA3EN3J#6R9 z^?%^L3-tM>ypk{1oy)fOPiL=6|*Ef0O@T zy3pO;QuA+ba&vP(M~}k1zv9Uq$7g+~`P+;)^2h(#Z*4ID+gn8Q`#x>vxIw4SbN5}c z$US!bQuoh3E8JTTRm0=g+rE$Pl+$?*%^8cIR7=oZ!(sCOv*yu`T)l3mUfupU5&XaF zZaCsS*OU4t_z7_x;IL$P7{Ac1^o4%@+vgqI^}&CaE?smdnDl>5{ww@r^nPX;Z$)L$ z^-gn-OCkYc+}As;k_(#t4|F5_AKf2O=lW0hzyGquuGfikU7uF-5|iJa(*C_kqqqY) z%xC_ekN-Ji(aAV%zV#8Gckh$tSy{bKpg*Y0ljpgWe;$k(Z$;jt${8! zxCQJ7O0PtEO1062=6^%ZU6@zaZ7o?(!qdc`G%xUVi0mBB^8MAGsJ$KreQ2MI_mAAU z^okee5q|qTJfG-Gd+X}|yghCB2akI&{+s@n_J5H5mGK6;-fYX0|1sv<`A^0GrVQn# z{{@fO4f?VDpW5XA)r;Bx1^>Nc{QEX~TlndX@$F0e^JrzB;eS>!{Qqgb6RvxoNIu}+ z=i_bVr@80#hmOObH{X0_6pu3dx1H|>{%J1f5V#w%<4pI|ZHt}gVmfr8ee3X-!+%sp zFZ$cx|2WxQN4_^5H93zieA#9PZOiiy+86p4Yrn$f-}~hGHQE=Y&Cy5S*7e6fxR?B= ziNKi;%%KKS*5rTY?=|G%pL*WfoPEW^CLlk0&&C9g zqY#&&+i2Xk+u#txBdALf-^0kqQyhbh;8ThQh(o--ZRXdz|1*Z~0pD*z-?#Ih;M48^ z0?(;7ZQ3OKpK)JzKiPfHUjKu!LhC=%|ENv=Sp|CcWT?mGYzwKZ%vZl!;^`wF*tUELFU&FxGs2wh0`d)|cgrW2c^i{gdgoTf5 z_%FyGzkY2NxN%Rct#$r?-F4UfioUxKIMm&JcK%mmJ9B@-KO!MJ|HnQ+inoe%7lh_v8fzE7^r^@8|tPn3^AL8+7VIH|`(nxF4v5a625Z zi?|cIfQG~`7EjsiX1un+jeU5n%?U?b#XOU_KtFU!Ce`5oTj>9bZ9Ea)Z#WEH3+584 zr>}F^2JiOmh~h-;5B-Y8i{eK4`tVYASbiVi+V7MF$gcBwR|pE^|KoW(M(;#?S@@Uu z58Lj5WB=F2e%=4X`(UZV4?jG0{`u$Y&5ty5L(~6)|LZ+5;os)}%mM2h|AqfPCq+2- z?eO*X-}SOvA79qa zMU#;Kp3VJbM^7`{-+a^*XqxdhrsX^vev3`LS|9kxR zYrW9Pc-Zst?*V(dGcvq6=ia z+qQfp_vst>roX(wWGmf{;4UD{9mmg3+TrV}dWG(OCr5P-+iBrJJbin(co-DM_ur8n zwO?bFFDtYMv|H$=QP)yG^uJ%>{U)9N`4YI)Mt(!a_)6yY_9hf(gVNwTb>^98ru0rM z`J|)|PCwWA|8nTW&i`rt2R&&1w}0*OAI^VVzKro)^^Nc!wl{JY!a#5<71z_yLh{7K ze5GHTg>GQ`MQ-T1i`}rl%y;))J{SGIB>`P*u{}cV3GFDqkSy_;TUS`$zwvPHEz%s( z`fu%Ztl7>|?TT@lv^j!RT`(xjU-1WaSmb8EUGylHkpy@2J;!^B{|Wy00*6gm`!&?s zU;STif=T{InEmM6X0s;L9MIg^HSViC(d^_9XS;q&HX~n(Ho(xK}Hu#-2EMwxhLQkFN$A0wl3xuTin{E zyvI?3E;d%b@7@}@*5vBlj+pFj`Xe%4WDULzzJA{Jb@Ac3@37u=w7*7MiEn;!x|98P zm}f*=cb>l3Eu5IYcMgkBNMu)cJD$L(_h(Wx~A_uvD!Uy-4e#s1;_}dS-#3Y z!ga1LeZ%%x`&6%7c&@#^z6vWAr!WoQR)6mwqzV0Fc&Tmo{$;6KF{kKJ93unXSscV% zUvKo-++VUE?dz(4)3?*S8G-eH)6{ z2D8{dXjgv^!!X?Y6|vPXg{PbTIF<2vZaxltzF}Ut`^r!^s~_(EZNx7YgY$W;?a`+p zuO7#sCwwILz(G?QZ<2ZysY8OVAYaS>q(!9FcqKbBzTAi|gSsU3@-&;PV=hhXXXPj! zd7ik)J$lUww_$bh^X{B)*gkje+^&rIlH2aaxNmpAf%`J){ebU;_Y3zeTeeIcd+f0( z-3h4kf9n6p0CZP~o&OU4F?IL<0RR14^X5YV+=}~wi#VRbiy5Dfr3VhT9yinQbjz_b zt>5@G2~Nl_^s{sYb+NekX>QTu*a+{!z38APG?k>GJk`&>J-)zeKk6#Ja`#zF+=z>p zxkoy$a8Gty>7Ko1mHTJU)$WCU)r>#Y?xlg%?$se{+-pNWLPLJjUoL&Vi8GoT#*G_y zI;ge#-@$(sXFm1*gXw*1-v`>TJzwD;x{&-QrSqWleY^V=8Zh|}---OcuKC}CG5$U5 zLt8iuzw700QuM_4c{NxsBLH#sLBV6~R-}OI*Ip=vx z-P1R(a$`nB>yV{0wz_I$4C`0&HW~9r-7UI}eMfua3;C4RoMu?JN{(s9`-OHG=H1@* z@%%S-cyDqz?#s;`JqvkD_z+O5d*A5BO&WmXa>@NU-A!##L5Tj#$NNBV!@UH3oQ{0~`Bo#%ff|9|j`2>-%q z&Mc1I-X;u~=fZ{mhz3GGx#b_(;se~#dOTzGQQXaR40?U^Q6E1_ALbd+m=E{1Xvw?1 zEtG)100psCxM&o=zudMtB)#`uvPSGbw4Z!$T; zr+~+uDw6xT7Qgn|YkC7_SLXAL*xRYpybs;3>|uu;mPX&t@UOQY{0~}5areKS|EACD z?HAMkfG^nj-}=n|g#U-GSQ+Er<5YHN1o!Z}lsktX;ZbFMRiC$S;|0^*Jr^x<3ny(Uxxcbg$oT{kA@{i#ncjZjb{E!rRrGnC z_bLPT8SZ;cGxnRTkE(d|r~I4G-0zv@47lEkxBWkOfYyI@|5IJ_|26Rc2>&7cKN`2z zQ@4O8@%`Q>itq1Cz9Igj^ul?Mqv`hnP4zmSIc0~rZu)D;)<45zd@)j@?=Rur*A^hw z4sJ=_%bZ?!J!DVhEqA@`qkCQof9m_X|DU}dlmBo>xZVE+&KtuI?ED9Egj&~sYnIUe zuT=l99XA6{S(e~WZvQ>7P3%5psre0zZR`6j<{Iwrz>au0afmNEP`pBVuzW4QTwaB7 zG@hvM-*U_>*3&tf8rXKJd;Q+(+V7_-0(|KuYae3EM$W0Q@3;xGaSaB!qBk2>zwg$s z3e9%DWkSDvx6;)0_Fr6`_c_DUr*-SrdZWqq`ZbS7rf2d#-sVmtzfJLWcp>`3Z{7cr zrQh4Tf8bts1}FPJwaI@Y{I3E(5pFzgbGQJ%F^==XVj3vrC)@~6w;szr3^*$lZ%6z^ z3wN|cUku#e(So_2(Gm32dYOB)^D6caipy0(v89A@2(jFE?`q)BmL3YePVH1%v(w|MltrYO7$l zBel}UoD#(w+-{j0_xOfdb%wRBUosVa zw1I2E&nkEK>C4@qwh`^9?X?=?S>ju8_X?zCVwNea7ZHpaJ76hNn__i|!&kDxb*h?T9AjKLDCipATuu;#2SYFJ6UA zJzpOngtu@%Wz;4&;v(9qGTKlV8Qi6euT!yhD=bXs^Sraea`$4twQlXQy7>^YzSX?< z^VgsJk#p2}OaFet9az}Cy% z^S7^cTQ`-S^eudEi@WcFmC$&JHWuJ>N9(A3?>cn_W664(JA|}X%nk0V+2^^MbNqUf zxe@br)9=w4PUu1RxJ#d_&VElkBLUq9t9t?Yu=^kB^Y->1TZ3BvH$(sP`cn6QMfhJG z!$!C*G=f=#^MP%`u$;e7Bl_rl0(*Smsb@=cx8aRGENA<4KCi-W@4v$Os?zzsfBSKB zU0pk5e^+KPuQJ+uRrZU5F{dZ{lGGu%I&@XbU%|lLK;l|vz z9{FNv8uWA!B`Xm^_EAy`aN?5lmDPEl0N(Fv+evB zG$CDKJO4rck|ouN|3|J8{%f`^hqGG3+UCL}LlTd^qd9BxR!j16AH*9Jx)Yq_@+mC0 z&k2j1aNnz?<`TkrF7Dl@t#ot8mflZaK8wEp7icMuc5~?q%c{?}FJF1y^XFCWy{CB- zu@?Rl;vN~_HK(0+n&BQkU&Vdy<&51~&G+El+Q+FE?$z<(`G$Y^hQ0l+xdUrJy%neT zKX#S;H}3z}mFoZNS8)ILHPw0eAKZ4O;m+HFiMfHhw}nH4LyQaIrPuMCBZEKp245bI ziWA0*@{0WmaPQ&j)5_L(az~5#Mpyk>Ew%D<^;+phT)4_Dn^}77pZ3yv+cQ@E!hECp zN9`>1_hGqmiqp$|7p!(uM{j1G=P_K`_Wd3?uJ89%;H?53&a#6KKG@#&{`Ieaoqp-1 zmyGWVzjghj14xM}=>%x}hjuh~w6}j@1G@j?ON;?udIf?q9ySY@cV4kNmB=#`UCL z{R7(ZT1kig#dB%nj;7y@d<+`sw~h*8kECE(7m${mO>y4KxM+djDx(_LQ%v4gXJE zyN3C2{a|os+f_~`gwY`QU~l@q@ZayGC3#_Cyd8B5XTn`6ni|x4rF-gzwI$ct(jy$* zcQy3G`2_0c>1IdkQ5gS`tE%0iiKTUTLN0*3em4E|j3bXcQg5*|2KNn+*O@NA=6c$@ z>(HS?y?MUyUF2u@XZ>LO9~x-L7@+r`4FAX$JHmf9hxPLAO~|?gL*YNdt?bCHc1O7O z_y+gwVq-KA`3vVgTP#TUzNe8w+ru&R5B+?33Ky=__m!{mm*3#nSRE7x{6BvSchZ+Q zZ=f;x+3stYH*miR`Q&(j&nv&JVfj&cQJ)YzRJO`}>c+Kh=`Cm2M>)HpRM0B$Iqg(l|kOCI~#2O?nB_+?f_t)D9yRBjP?u2vx0e~&Wr2q zaC`p|eUY=kf6+q+>LbtfSmy?}5q+=D_3y-;^Bas`NE3pKhi%^z#rLB=66K?Gq4D|0 zejfHQE`6VtEgbbZfivmgYk+7f$tRyCRo*LuHn{C2=3k=i$2x26w~}=u`XbPP>TC71 zx>A><&c5#0Vcl>K`O|8nGp&{D@e}S}0p@z3S8uS|Uasc(oZZhd-^)l}`-vx>u>HGt z-+lMPpg+Z30eTBCjSN6*N8JIbdjfP{NE5~Y;r}bVDcJ%17d>>e9eMaaZ(9%kLtF_i zb$o*p$vxCRWwd_G;i({Pg1@ z5v^~Pr`1(FpdNJ^Oc_H?TkYN)zM)=n9nG=O;dzWZxpaO{_x9M?9j)(=KKkhFWtUy1 zx0^rgSf58PtQk`55!)FMbcCcM>~%n!F#hZP-@PAu?6DJ=gXUk(5)<-vrZF$|-4Nm5 zV4B0xUG1t3PTuw~)3@*Yf@%L&;`_ODdF}!@-&OGK^OUVH-}lx3HMWHD3+tgYWA2qc zW$|9Qj z-FM%S;D2SVwmFwz_xg~Hv^Q$EhlT9W2p)F@an#?1Bjfw@QJ*ftV}9B&pW56iL%31C zo~C@hnx76hS^EI7!7cvtCNkGj_^*avYp-f}nmoRx0k zGn?uq*8%tI(BJ4kV#J8=0q@4};D*9IbG@>I4mv2K{dxR9(thtrM*P2P*RE;giD~q} zvXpCgz%k!o3ed>zfEzSu&>_74IX{Q>oJ%^c?>xxeiS5BzfZy8iBz`J93HO3mUfg=- zk(VZ>HQ~6=2}{VAF*>FxA5UQ_PvPF^`*rqjOX45hmisPZj5=O8S2}Np?uZ_Io_&sA z;)a~H%uRfjc?Ijox_-eV!JNd_|iB=*J2defT~-_QBHo zzSZpWkGvSZe-b=Hyo2;I{Z8b(8?+-DHoqWj!{yLeK(?gG)fz`y+EFHHsr4eHE5L)INlDR-}nFTVK4 zy#M`5srJl%W2+l_n(*y$?D1G&w+Bzw_m@RuC~F3fr@IMD+;KiT@;#8XyvM7?fx@(+ zgNW}$We-x>lsWE^%_aB!*DPWDzigem>y%vG)JCNh4fKP4m8bkfUyoh2jZ={WzRkL+$`%p$z)BIJ7vn0j|=^lFZD`E2M{_G z{%sC`&Gf%&7b5I=#1Th)Yt^b%kA?LQ-Akr!clVvQ78nF@5QvsG-df0#FxxR<*VP`MgDc-|8nrJcExouT$AU3pdF&MXS!|J z5xLGZWP0tH*K2;=nEl%->0UCo%U*clg&EEBrI+_f{dnIw^}quUq$E>d&XGm7RL+@E zn*$;X+JoP=;_R+-vO79HD;r_wC8$DL&@Mh3C2>*ICF!s2)Go_HQEBIR@FS z&Sy4)-!~AvCD%dzYH#x)|Nz&^+! zJ2Ou$Dk+hi?D_5+;gcRtHEqKaeMT^gcyG-(^`?OUr{cc@TCvBE5g*9QL$u=K25wJ> zntQ(c@TS`N|K+0VTt1$j?;^ju+OKk>dabRmOz(Yg&r`au9t=`-CwC+8TS-4JXAI8X zc;k&3_I1<9`9Bey7c1nbqmD`=1Jpf`CI@C7sNb%{-HUT$t(mLlUz#4%yXMbtkKq0k z>yr_z1g9bG*O`&O$J4-*S6ZJ{nEWkHhg>|LCbT^cLqECi1_y&$qk9PMh10_DQr}9a z`GR=(*ay-5DA9Fo{J&zoySr_E{gi&#>FZcKa<3$MzIFZRH(QbGJThw3s9zyd)qUKJ zxP!5hd0shkSKY~!(fG?8tA>t#F+6=7e(<9d#?FDW-johmxp)({<^a3jfB*dlvId-8 zs;zUzY<2gX37pzRuu5PLZbNvB?@s`-XMT@g>&*ulKOF*PR?qw6+)S*}pB5&K2J}cagsH&O1}+ z;d~sZtyiA#c-;k&;*BW11)_Tb%aA=)QoqJzup9EBZ_|#aOW}Xr^7q}7*8(f>KeWU8 z#0dU_!IN+_DDI;Q7c5KRCiEW~#ItlUJqX|G`^r~z;LEH}zCIt(z&o|{|Fz8j4ga2| zu}5COU31TEb=&HpuLJJaBG>7A&pr3(OlD(X+EC|mkmZ$0*1|rH=6N6M`mSEcClb0o z7FDt)odsfil1@>D=oLA1Q)uE#>@nUzn~E|#`A&FjiyQVQaN9nIe~%~OO5>=`!ntrC^8KRmgSzB}<3FUsTKCRFrN{pW{~O_HQJWsTXoH(JYHNMvI%|;YbdBXY zO{BBI-dZL5^`@uCJUh*M9O(xid@#kGEcHdx#~u${8~(w!(*tZ zc!1`>Ph7nb9uVPQcom+6d)YZWZ;0u@+c|p4!%@ITLVwYe@II)u*7*@lC3(mA*0=W? zQY0S>J9Rztf6n?9rD*3o{_k$Pp0)i(H*YLwfs4+7^C9BpI`B-B>(Gy?z<HA47^GAhEyOEZb-accqi)H>`^?E-`d=l^rdFLvdxV+ zcZ2mAkEg+{!M%+O!Z)UUY|EYCCiL@e(S>(Mws3hT@)+S>c7)>~j_|Mi<1&UMWyF3) zACY?)_#gl1){^+w{O_?VHo7OfY;=n!ZLgPH2c3;M$aT)4o7a_F`J<3kfFx%;eHv(9AiK&#+TB)QbPL%iecm zM{IE`=agU&Inp;5xSs}m+S7L=*U{ZO4YaS8*#q)G<}ruU|5HZF`i^^G+ab}k=x@Y z7gm5T;g7y68H2`>yKOF!i(~i|PTP||rZ7MMEjx(|{wLJZ|FySmI{9@^^BsDZZ%JPV zm}$>cIvWkpt1;c|;lqb#z`tZVb)Ly6fsaqzn>DJP3D&)F)LZf(I~RmJU^i&|3}gXC z_k%qlY-jIl!T4=%^zEFRItP3RC+%X}a1hafr-Ay|>igpPq5<{m`lN~QUA(N}r*5nj z|Gq7C*zdD9Cb`Zb44qAZk90N~Fy5E3f0LDNEqpZHyLazT9ETsWz%)ERExiN18^>C( zT?jM~uyZg`CUtF;a+2YUYt`m<$o7RKdeJR9LgC^y77;)UsFH!o@-R>VjSGceCVO)o_p?p(bLj~ zNki81mDIR~LOC`_;WbwjMw(XybtPg-F-r*=L{qne^0Z0HuV- zHck($UHXArIDWgE_>XPwjXSrxQMYVyPj%Vs9__fxEJn()8S?!S{W|?_*>!Tj%|x8H~A@Fm7(%fmz}{sxZjLCWSE!h zuwJ%%c9}1i={yed{It$s;{UnC{oQxpof6ODj$qRbXC9l;T|&|ig~#bVsa?1i;3(!i z6HB1Hq_@@yk{NH|yxaQaAGp=?-*?MrzHf7E@rv26Zg(?Y*zTr2x6Ms{dYgOi@vUw= zvW4-FZgKBEu+_aga$A8Z=2sYJn;ZYgHaGE!ZEngl+uY2Twz~!Ia0kPRdJOIbEs-3Y z{m@$m4jlMx^#%HPL*~f#zK(b$a#P#8l}wknm<^6~^|jAhI89Du(cn9c{%Ja@M`tfoicGg_0hdA)d1G^3)u_3;-ZT#+83NP z5}h?^-0ySQ=Zn!Gc(!*UHHYCIA>AXE#a+g{K>LGL$g+2N`|Y<6W=-%U zJhA8*u~M-7ocVtMoljujz8(8D(u=meTj}fQ?Ih0NWb}?Ad{O6bi-GcU>4z*M1+Pnu z9XnQY!L;^>kYQxdMK{?H^r5k!G5bT`CEoS)k9pvA$C>J9m>8hzP0;saZ@u-_@0ibP zKU8DAt?eZvL6)1j{PN4w+Q0o=BDt1IgBBYHv^R-vcuHr>*~2hhF=Rto>8-#6wJxaQ zO{qOOD|#q#2S5kX7p-%B@EJzCs`dfxn6+ZXicaj$eUmmCztAtH^#rrxs*e=m+D5~@p>#P_&m`9 zXx}~`I_^(9eurL!WI7Ec&tp%-aL+s_t8>?q;4%eN5-EcICdb-=Pz&Gq(+qgXsOlO7N_+_F3jY8RX+>=6dM` z3l@AK@b0Mrh$ropoB2>$=f{pY<``Qa=)O_rgyqPKDx^b3IvX2U2RA!WWd6M_*l=BQ_-b?x2IR|q#cWM2O_53&CZPI&&tyMKpJ4Dyov**72 ztmZznHI443+HSCbZajFtP&PQUx#5f%Gg90;od)k|&L`SBK{61n6(tK}?O4G)S>uDJ z2crpq)d-m>qUOd;;Ges57GZDhtlGDK|NaNSTfQY)dGygofAEig{Nq2Le){SELeBOR z8O|gAgz?sFeanscG5$aH{@#{f9A9Do&6`(0B){*2=Wq4x+xKf1TyVi&@GR|ZHNt58 zvI`JsO9gGv{R&y>=MgWXdy<&<{maeqX!A$*6D=@bl0K8kjyY4B(RpS1j^tux%$3Wf zSD_F1=xBaqaQauwC>$Fk_tFpDk(ACW)9=##`t{4SZQE9Q1sVFB*2}iH zq&-HRJw|4(IYb$2tTOp&9MS%>Z2vx(gbM zbg!-Es1}#GO1b7MDo^_~y7vUWm1X^sW$vmuU8Y^Tc4@|lG-E}IzMo=mFB$j#6>;}* zwuv=jYWD2eDRjv7?xgrpsTF8|`@_a=Xwu==9qt#cBR7mn^c)jJo=^|a@jX8mD?^Yf;A!J013JRtqZBaft6 zlceA~Deg$lEBA|rgMYX8iwObxPR(C5yY>FtJ!G1i&jys#tMJ@s zNVijh zN8#5wKmWq?*}B3D%BzvSuJAhYuPjKvqu)&n(ieU=D+n+AKCmFXuJ29-@m>CRy5Kuo z$9L0$@MZ5SaHF( z6&F;{2FGB$Yi7xF73vgy`vT+alMLhRlMLeQNEFq`Cyz@^qASagIzQVkLIf6i1G=R=t4IImleZaJ2)Y{wO-c8A=E#BVTi9Ke_n9Q};UaPlJ z5IJQs=)PpGyY4#a`02oGn4i9NC$F99X1~+Ek#kB%BE#O7eJf6oF7ZRhAalwor`R|8 zG?owhrx|3VR<6>qPh#hV<*#p*fvnQ*0oFbXGIzbNcLKG|xg&UmU%@)m2l zW5z(+re%2d!uDz9$3AXa{80NyqC3WZ z+s73j7JahMEc!`|^cZ;`Jo3PIs_bdfJkuAL{&7R%xM4-Ob_CmMP4a z5AE6>s`6GH(YwfM-%IsSz3C%XAN4cH+lGT3q#gogY=dVa_O+hy6b34}_<=Bf)S<8HqpcfBzx~7)-Qhg^pT! z`t<4INjjUUxq$SJbsxCiZpwgz4Cix=H)$_h^ulhd1%5E(5*lJ!0t;+TJ~FmM)R)@k6JUH*HAD_xZ_Bbz}^&Fx83vWZ%*k zfVQoU%3u9X`G@)Fo4KI!HyF^T4JLvWeZpWs9%VWYB|}}x1TXrig%MBhFX|4*Ed3zM z+{tht|JWpbK{#U$s(W6opMV>i=MgXL3!$5!FVs(M*I1oKZ!5!FMK*q;f0sr7()f*d zEpZG7($CcS6!=*gyi)g^(WXjxbp?5v{*=;FU(4I)rM#5~O)&i?Uw73>?bT29QCa%=bh3RL z)F%2|B{W$H%nVkfsfhb_1u&K_wAF()Dog9H`4^Cf+ET$G=ScmUQB|6@adSunPE`;SX9idJ>L_Bf8ZcZwBN!N+{9}@ zUJZyReaHsjLGJ=K0RLjVRZ2&m7Dhh$t|Clt2R8t>7UtvPr?4t;FMo3@52Xu?l}~w9 zlvf3PFmGl>CKQ<`C6Fjqqr(xag;|~ zm!uxRq)Kg68o@*5D{r+?20WN8e~h}3=@F`*G8PyQq278=#b8f9)>kOk#vk(bTZGx@kCHyG*7sAuHibQQ_+BME!>NCpjEwf)et=Cjw9W5W^UQU zGe6=sB3&c=WH;6~am~-W^-Vr{r$@wTVdQD?6edG?jpV1iXpb4iRUWWI zh%oE3z=*7>su&B6ACQ;zAHuDV5M+G>Sm>Q8jU^g;lQ|rC)SS!KBbvX;oXYx*`IW8l z!DZM7LN`_UT>hj0pVnC-TnrEP+ zT}Wf`6jyE;%1Yewr(E5UVeyoQ(kWkzY%`SW{dbXD<*81jOUhQ>ijTjAs~pAk_0qS( z^{u#yr?PywPp@yq3)*C5__nJ|!VM;*w>prw-o-ILACEA-mn{LY?#UB>XiPfm=frPF znDrO@g$Io{$vC2M3b@#uk$kOBkdLjGG_Es0GnoQ&*$g_$CPNc#Xx%_N(zK0iBmPO= zpQoSoUDignH*?cXH<@lYvSrf?*F2t|<~3!^F*T1j18n?QYI7GFEWTp=9DFq-jrfuP zZnO=o8;gE~N8EXILORh$Q}S#I2%E}{pT2h`uHL`Y8>qYC*7vUDvnz3SB~EB|BmQpq zTRep+4*AL7-1;W%Zq#Wv*@{cJ<*6{m^Ks=bKa~ePm>rgwOn*EMFhTr-^NA3vf;9iNEeTYD#>A9r+4&*3XFN;cWecvIKkIKWtp09Sv!R z^$X3<_|aSjT!s}HemWmw{Qz&1myw6b(KHswNbaFNK^?q|l`4ELen$KBdy^TT$(<3< zxXG?1d!zkX!X%$l-xu9$Zewdn;AZns!Zd#oKkAg1+I`90R@0z{#p0n?{H|Iv+efj@ApU<4xd-`7UtTk)8 z(q2z_tS6eUulO4%{sufN3>|0u&FG!85H`>|hI8-MS3c`2>;{Cz;E%2JER2UEuA3}> zi?48`y@B#Ef8x8rk2wC0O}TK--10p2PB|=1p2?qb@SKI+KxMVEXJI$U^0sfw!dM!_ z%kr@JdS6dvG#V)lvvE^?7?TNlhqlItin|VVB0t(38f$n1*#uvpXBjW3-mU)v4`|$i zcERMG(xD%1<58*)!-#Ovn6(3rf7@5VK89?pT+#kd{uraTK92To*gT5#6dTJ?pVs=a zq6d2h+i8rB{uAR6$Od#w(`Q6`aKDyl3@xFn$sxU)T+3*0bp+lN#&9WWICeHveC{{p z9eX3Cv5~@Z=k-SNWA88KvvM<|G|X1G&!$FUK1Vo=;(m_sDkt{ml+I^m%q{aHY!f3t zOB)+Q91qVk>5x7)?y1T0$=%B76PCD{yW8fb@Aw?wlKI>B^2|46ZrDZ(X^QnH44~ZXucd>hwrfY8TDfs!(mS_V|~Z~FwfWt^e~-9anWPZx2>I) zADB0+%g^Ma-ajW=eqQ$HEc`=*(}e^ZS7&G790#8F<%h$?C zxGX(O3qQHJNyCso-|pXP-;bNZ8%>ms)5hqhcc-J#R^PRWXl?wcI3`zAHtNIMB5O+& z#$=V!MsIU{6up&pf$;|$Z&P~o?QFbRSYb}1o%+sgH3x0;K1$np06WK-^at04KJj&X zK9;ZbC9Ju!Iq}eA}jpxsvuD_?Yr-O|<<@0Ca@hp>n zv)ndU9Z*)j8T#O7bW*s@FqwM?*(ohIRXBJw=O4XO2gnw0n~+J~e&jqMH`AFE*7|Eo z109L+3ma3?v$Y542l7Kl=4^S+w%k4^Y2&Nub`O@%j;`F#^VkCQx;Hh$lfpV?{to(?vKuvvI3k7#G< zk{;#Lds+@FANk{l;hFS#CJyg1(8AoXWsD~B|GL7NQQ3@sa{HS6zlu@3oTuaut&L}7 zQwJSnoG-ql@L$xs>4bWweKs9RVd-0JD16YaBkySM!GVn<>3dd}ZpDkuuX&qhz_ZsOP^RfS2?(1A3+q2HvUiu~5=}uRb*|4udaHcx*;rh&W z%${;TwCx~!Ga2fDy1-CJR+sXp ze!SrvU2)& z8C?|4Z25mj>3m1u%KNwFZbr}0^PBRA#_){E2-)z?*W`|zLH5w5Y$kW=%k(w9qnlBm z)GK{08#9p2LMr0}g<))i`3h?bl$Z6BX$$0MYcCYG4eGOag>?XorQ3Qiou@*3@QLD| zGefz{RBNOf#ILrlQEe4%QyZ1n*4>B)Y`#i1*yp?WKE}Xp>``S#&tbd;-7&|!;zPF% z^4nbQUy;9IT{iOWo1($Dgi$bUxK>!JXZcg-;OBdK{=OT%Z=o<|^!x+G`+@v^;Lp6P zEI*XN?}y66!r-rXZp>frKg4)g;u6Qx@^rFrc{=Vd`-g-lK50`9((v~zJ@exkf69X) zF6qe4((tl)9NCtxr-kj$9)`GC8S=7`2SzmAf_x~K;u*aZ@B4b+Lhs+#^Y<`{pYyZc zjZb8MOW~|-Qd-`I7(Xa3dS?e2`XTf)XcsnCIQrC#$I*9Y>~&ok=4O~*L_dZH7!zk6 z)Xr~WzEpT?!`ZB?7l#KF-tqr&^8o2r*w3WKE;DhKgQ_UjLP{V zmG6fbJ;Ntk$c9g-3zs|A?kG*#jc+L~Iw19kPWlza-Asn|09}^89J24T;6ePyd&UDn{ed|jFvPs_s#-t9IrO&vPIJbrg|vI86b@a;?sL!@SS9(<|5UD3R9@R(q-XBF z;BHIw9o8|}xW1lQzrcVdx?8I2)6ZE~{LENAZ64!ieD@cigA6*(W-?!uKeR$OMxM|X zqaQ$9mrck%Fb=+{qa0(3!}EV-=4Y~-Ge4IfX3LVPE`!AAs>2s~H?$%tM?*H%D z%zL1TaGJa4Un%<+bhjbr<-L3u4E@`U^qq{$)AoHq-^ceO z4;ZhY%|xd}ZlJS%U(Z{}jdmJ&jvR;9UJq{nO#Z)+4EUwS34SH{u&w$dztQ~p_F8kh zqxN|0tiH+4+T*&r_6+W;eJ}@TkJI5AJ2+fx(+^kw=_swq`)|?jk6pWV#T<*_UB+zO zvv>U6y&Q&k=I>!j?|B**U3AeOWsb#=hr)SWd(ZMM=bifHX?eM@Nhh;;Hj}1BzV0sj z*v_3h|3Uj&j)V@-Lv!^9b?n$tWZG{Z?HSrtXO8VG8M~9}d_Ud2r_v`wPonSk6}i*qGnT~ok@XFf&W3ttE}r!y z%mb~du&gy=?9g-}Va)o(vf0mL^G)K3k6RD2JH)d+f+|u2_XgSBL0Zd%+-FTX^LmWy zGp@zFtz-YQ`VM#;-4B^h|ELQ0it|1B(Vn7zB2RuI+hsa(=;z9B8|p;$w5QJe>!^Dx zkJO&^6EzNXp3aNy@zhgK4S4zGmv7X0zk_r(^X)OH#*OBgvQUtYW!?mGX}qw{F{r{DtGPiY*qNAtSenUefJoB|7%VQ?fZRc)~s32 z%RHy?y(gwlojP>l#EB1UU($U#Gy7KEk#L>p+yB1%?z{NL8*luZ_HzD7dz22>JpTdO zhrEaSp*u=9-lwsb$ z`vk1bTSM=xecJOi7Q96B;>&eD;(I!``UCAL)7?7% zpSrF?!*8Gb4;qS|?~AVQh`!5orobZ6d!Fb%L*sxG;UArW`iRCv{-J$L7hiSNRVQiR z;}KfFwwvVQFIC^p53~vN!5BlJKaGCNxDq-qvao~d{7b?yb8F0PZX`G30olx(u?E%F znaXDEKs)LE?3rsre}FxiM;>{k?KhLZ^;Knm6u*kbFzsws@i*()SgUX2u}U01MDKww zP3NJkdPeRuZ)@0>8@$aN=H@btDKnPF7$SYA9QW#1Y^nX$J0e%^y6djynRe z2;YmTgACiMmj7V;ljnxoIZ_$UBgi8CURT@kpzZ^>Qs=y$cJ$Fl@27PO+o|s11?yW2 z&*-++76=E7?R{1F-wYnmGkwF)E9^!xjG3&fcjk?a2lU>aHDsIWENEX#%|37T`)BJ$ zKT`Zl#?!Vi=40y`#BXg>9_#PQO+{|cSQUC2d`g>Ta$nEPVS#5$)lR z6!s@dZyU9v``mHI9e~$7@gW`B!K*3k;C;ZvD3_7pd`n^h}QDtBso9*vZdFhLDe+oK3drVl%u&(+6>nj|6 zUCPeftzlnoU)Q&R{aoftm->bJpa<)WhhC!5bINlra%4qx{g3~rj{>ya;)UsI7w*^D z-Dm3@gFS>7`gM#SF^2s;;p1Dv3HrfiQg6_e(S=yYYJFmbH6BoScz`h>7SY(=WA>JD zr-$aG*kfk+*S@Qj1HWSFUXoR$>oCv7{qEdrVB?*_JL?{% zt*@(ZF`WnO%Z)zI_m%Vyl*Z3Bf3fe-p+mb#e;ls62$uavpZWA*dSyzCs-!-|Lg}@S z=pLLiG={m8@J`>rctAK}4&Qh{&#a|@2hgL@r#ZZ0ZQ+cy@is>+KjQ(;_|P3jHXce} zfOQcn-@m&yj(uni$vGD`)~PbIQ@Pl?&6*{!k8Z|z6>I)Jr?OFI<~dkjMgN9=F4+H( z&Li1Y<29#hjA1Br_z&(^21TF#IPih`(9?C#@a-B)I#O$fnTKHh0Nt3e@UICotSy2E z7#n4NfW02n0s`rplE_y))R4|Sf^E;@Ixzs7AQNzeH- z-JAI72i*VBFF-F+fA1xY9i1zEWn1CM`UAo`b7o%^mNt_-MUQ4oXhUI&c~sU-vag0S z1=v4jwk#?=m5H}ai;pn zSLmFeU4=8ohFGgdJHR{{bERL9+-L8{hQj~4lJRQ``}}NPOgPKu1d!L7)3ddi8U9$_c0#^&4z{dG4Dq6 z-a8+Lcb0z;-hT7_@bn8WRy2$BUe7nYx#+#BZ-)-= zzV$(PFQbps2^-o$L+JTGqAPOx_dO*1Pm`*{9afp+|Hl&c9{J7;xO{5IbqlX z)5EiOO$*Q7IV(JK+wAbt=+9FP|13=sPt$*l%jM_p-_1p6-RY`_jqbjxH0! z9T)zahBVyNn^wK~CgBEko)E4-{q=Cg2``4rj~^BWo-{n%bk3OO%b&#kXV;1L?Sr%L z$G)%El<+`5%+&D6mD9pQS56Lt`;HBdTsuBIe#_+W;{CJ2*k>1qi7zh=vnPn(X1)`a z&VMhwwN$+G_6NorA7%5B@xX1G&)rElGd)O{V(!$&M1?ih&9i2j`KGmGn0sA~v+3l{ zycB&^WWSr%$baJ(@XtP8=~tZ1!g?e2uD2I2G6&5(+}e6(Jc{{=ja4S=^9uK@dHvq+ ze)qebCrz4k=SQLQAk&vgZq1qaR+#wGlJN3_^THE1%nbMUo)QLKFd_W?l(C`z?_UWQ z?)^kKXXhc|tR04g3wM7uT-Ncm(BpttL-+k&4VUiyO6a=h%elE^&sRcj_R@2U=H=+` zaR|$^$EoMp!^*z2R(O@|5;Ix4o>$pc{0o0OEd2Fn4~M_};65{_Z8bDpwA=9Fd$RN| zB~Oo^04_J548b_hd#MG z8SJCCFP`&mc=eG50lnm!E1HJJ9alWUdQ{s7CP_pLOA*7j|dkJ7(UM0 z?g`_MZt`$utP_ARYl+fDu5p(=Z;KA7O>lp`Kn82)+DXYU%PUQ!9@&KYxyCd3bCuQ}#?+mR`v{ z59j5{(r~<&v9IvxM`!-(ag)dJ_^H2mG<4Z@SW)h(Z;Scaw=^k({5&6v<9Ww2JAFFs z^qlEg;j8>jT7T3SW>Ij~A)!K)F$Bwps*Tk2WT6@r19+3Y0w&uvL z(Vb0O374#!{EAc%#)9YvGw#Cpi?5Ai-xcR9BLCaynKcWJ{}uKDR5lo-!k%KSv*%7J z`o5gih+bu5p5i$>vqU-;=La$#YvcV2^L5<;@sr`hhj&x|rB!+ly3U+QZ>jG!UHZ*v z!;8sy$7F6(r(RdWO^z$CgV#cr-Cqo+{9=e7d0hVv{|4UmOxqy&)notH z!?nkc4bR;%$Hu5y%LCFSrrvPF4aW$3><6^5Alb~bGVa2hFmq7MMX^_mHbBnp*o*G? zcOC%W&Hkio;l~|_?5VVSa+C&Rp0>tWa-OraIme4LT-c+)Iv>V7ZOljGrTb}3_-n2C zKCq9DKdAG>(6giToV_D0%UFx=Mg10a?zBfoiqGfm@N78gzaF-F!#-cQalhI>8B7 zjqx;}_#E>|-7U=wqn{&)V)ulkMfO<#*33cUQgkIc#KjXu$NaVEWsw z>LW`x_)zPG?$r8$UkQJ#m1ZpHi?W$>vVFtCBKzi9i^g6Q&ei1XsaD}%X$wv(MpNLI`o=g7 zv2!|RSSr7#VHwLfL-;@sI`JnD8E!5XeuxuIb^3*S%lVY^tQF7Q%4K%p*rvQqJ4|1g_U1(K9sR2d)CciA zy6r>UYW^wGEy`zkiU)cfJR&^MXIfY=($5({EFAI+w3!fWG z7g|^E9Jpin7anacOyf3fw1$VfRO*udiUf=QuAhJD9^;Gy3^^4mDJJT0_ z)bIjs&fWf*TyEj->3DqVG1g`DtM-reXmyzLVy!e{nZ5p_-s0&M81p<0H#tuhSn>Gg zul%~~`eHbBtA9m)HW~bsbUO63a+>9GM*sLNv25vgrO%#cK?DE3*VONgZ4bPMOhKn) z3@GOB-yHcMmN_j~Q6|en-_z@$5rMH&=IiSHv0iM_q)9hxp9^~j7zbj%*e1gMM#3WU zm~%#v&+I?v3=-~IvwQu}XXNg(U5_>IdKRC~2 zbB+0YM{Ai+*B-6;^?uKr3*Iw*|FVuFa@jDw(M| zJM|a;(ikai0AXETHI3)#I}Y;v4W}`@janch)=P!@T1HYyybZ@hjGf5@f-fCZ(tlAo^bzKc8~p9$luGDl*8L8 z(n_DxveJ+J+qQ)~VXmd?9*ip#Wp1_}lf10FqA@)1k5k8oX``0atp}l7YmHo2>E_=> z?h1?aJsE#z{TX}C?YtE96}{Vie(aA_Sk`gcedyw~=Jo_swAQVnvqJ1Xan2rP-V6N0 zQ{sU&g?BsCLGO$^q5E`D+Fu(uaNvRB%@OtTF}mL!ohRj3bDD!2GpfH7L;17OEn<{0 zh06{YksInNHxa=KPy+_dJMy;Vv3m`}q*Hrw^+wc+_;t zGQZ~eK#Lk_=4Dk~Qh)!tdZyN4)xQ*8W9tuSZSuCrU18Ar0LWwHvhZo^;q-2M0vNkw zZzTBFd{*W73SObXAK@_oNg|+ zZ$A6%vuBGpm(_!R*6zG?|NPMZh|xLzon}sJWX@5>q z|C=(QQ_?r7icv2kfmB)NOw|XRvTK-3Hv=$i?tF1|66>ZIwzk_JG~Hf2;rYL;DQp4EN}4P#>=Gl%k~=?@$7zX zdzgA{;wfxRT$a)#&#rsCV*O0`7IW6NntNg_-NVYx)2fwrQm&f#OVUZ{o!3K_X8xX~ z7yH7GnLf!l)Y;p`Ik74lCV5xIuaRd@wE<7xGRNw$9t<%b_QVrUoUT4C<3OypL=Ulf z0JQ;zf8mlfnp{|E_kc_Gb4G20&itxi&OiTrdB>mfvr|Kzt2>+w?YfHmbe;ga}FM3qr%jxEH(tE$d zM~5K;XNFnhmRnzGMSTokD>wF;#bHpVNoq4j8E*b2J%TnHbH>)piKz`p>)7*j+slHT zx;6JKK7-e~?;Q=gSo&U*-$^S?KYItS^e<1At^xLuv*;d#y=>o+W>Q=k4LvPS54)C8 zIKroNw=^@i+?~8?@IEs8{2ghR)F)BdPE*-BOQ)=vS2OviVWG>dXHN|Cr@mbmgjD|@ zOqnv}A3A%SbE1527-z}aS|DN9&Pb6T_;07P0NZeGq2xAe`77GPQjuO1!LNVtFJpZJ ztygZ)x=ow+72ewj@9mTxeF@f9Fuz9Mm-&Xg@2j@x0PW+QQBUFN>pwSWZs;j_?lf@P zrxm*MW{?TF2-$COS zs>?Lr{MoUDof#`r`kd9RxvLI)$iS;t|7l$K`$^-&&3~H^Za;5Qxasr>;aauv=w&@S zjx_$@+tc(s4{Y2o);QG}zkJ-v3eM0u=r7{etbs2Q|2&T}2D3aoY*pT+G&~HphvWH@ zJzrJZ^n~#y^hCCF+bfpO>ovEbSxT#nhE_i12mY@-W~|!nB|2xTz9lmnXMg^L6HfS* z@VJ?<$(dQ4C(D?;-6bG=vOmT809vQU7`~m8sPQ1vakSr9QR7eiWcb(lffb!6T~Xhf z`zP&OTZLfm*Uqw49=^w&vnV*Pd{do+^^GAzhMcH2>8*P3zjWSv;l3Wzj2=!~qeF?l zJw%rQM~(?E+%>ONeWR)}h?Lr`Ve>45yG@l2H9~#iC{s`V*^qGhR*%_usMTvT_3n7| zvZ9BjZj`fE$5G+xW5U7`W-g9 ziUxW5!UjBf?r)wo`3knY&r2B3yXXF+BttL-=ADj3T7_Tg=2)ccsk^10@nzY){7xDd z{#J7mlKqtN)L#s>`IcH`@ci;Lk}`XqeGVEGo@s>+r1cQv`}FCvmoUga!!Jwk0soB2 zuOl6XJB!(yYxw7^Ho0@(c7xXSxQ?%=RX=mrnWu(-t$k?2x;MpT?$_|Iwu3$Qw&qRv z=j`eZn)BU4XKh}lxrz6xu-D8BIQYlO6HHb*J*w&z4Q!NyLaTpeOq1|cgj|(y8%ay36Bhz5hlF2G_G;f z+SRC^Rm(BlkZtU-nm_HG@bUu-)aFkLJr5Wa^_eVx^6Ijy)^UqB;n7pJ{8#8CUBhsd zBLwi7<#rd+FrpL9il+vW_9DM0i(XhUw zpES=jM{@TB=`x(5Y5O9D@ePGp+ZQi4?n7rk80TYgXGWvW9cj4ky6Y;kD>@Ih3jBI8 z{A<5UMQ8apC_ZCGtovM5yk+~n=v%PnQ~a=z(l-3-E`a~ixc?y4?+Pa|sDIkHD~=v( z_-~Fr(EjE#Cx%&X)YYW4h<%BFYXd&~;KT6h!wXG60Z*rHIaK)nmvHuY=(5Z3D*Rjh zWSH?fbKA>I9p5N^8u{3wsGHT#i|+_QkLtfb&tnKmAkgQd~oGvh8(Nvz?bN z(cP%CzZ-PEU`1#7)EfUc{3{*KkK;UDHn+3q%g%Hb_E}fK-5l$Q&pzw;|I?rTw6)G6 zezv8ux*xj#FzGXW4;>w4p2@S6Mx!L>MuxtJj1JG=G0*nvdfb24M*c3E{cgDJoJpof zqpyO0>g=p-pSF1etNW@lITlhIoQ?`NpFS~69KO`{$F6MIC=>b-I?;e5kqIM1=beYC zKlzxI4ZE{+jEI-C4CQBVDK~h8^5@^+;T*T|y$Logd<^4$(XNgcc>_1sy+)bt4(45+ zFs{HHd*nOMKZdXVj+l)~$Dc;@%OKl-?2_C~@vYZK_F zau4PuJKI^|Iak!~D^)t|tFf~^!M~DV-vMKvn_Y6r zCA(;k&X^UI7yq~4{2&aGyt6jn>kC@CQT=rn9sYjegfMMn{d0o# zI9fQ<{=ThZ8*rL*S8$hOh_WRn%1g)2sBpt6!oJ3^n8)}ie#;iVAD;O8Y|RC|9{#q? zli^h3%b}X9R-bpT*E88)U8b~rRnOK=WM$3hn)>rCZgTe~ZjMdIZOeJn7pB}%o(wge zh(2vfw>+}P5{VQ3Gi$DaF-j{MjJ6A=Rg z|1i#Uo)6!zua4?1AL9adT=a&G_k*J|e))vt=*yX`ReM?DN2gcnZqHz8Kf?1JncWH>^=-LsxV*Vnuu-rzXE#{wpo+ zp>NANBK^2~V@z`$9MLlV@Hq5*!Z>t}yeFoyQi+9)j($-HH{?Ro$?f8P}Usgv}?_ecjiTdh& z;28DEXW5wtt<85u;c3s`cyVFqez4~0em>OnXp^Ci;T$&wy<$0I86&QQORz-Q32(eC zJA1y&zQwVdeH&qF8O7n7V_asQek)Rn${ z`!)f;9OwgZW(xb}S^Hr9U+Ma*i>vy*p~0Q?lv3w?7`f^fHbgz9!bw>Fgg>lYU{-W^Cy~5lV!{lz2{?8g{-6Ldk0E}^J+^7tG8~;&T z!PqC~y`eMN8P6I|vVAXdUPsvHUJJv&_6K}>!h{J|urHy-g36P#!^S+hI9&CIH%ykM zH0ZUz=0!6)^f_ouc={&f{|b{_QI<9R3;Zk3{V=07k2BonTEV~R-I8NfSW%eEj~o|X zQ{U|4{&DUF1)&fP7J$4%_qKaG>o0l!)XI+=SGjxyxwl*)F= z?(~J93a1JGw5y$Vc){x1!h4?B*>j$b-Y*mWZ#iR9n4>wOdVcIN*L`FsOI6uic;&tf z#$`AYs|x?D7)K^vmdw(tziO`_Gh){c+*{y-fZ`>{AbnJD&UN zXRXbq&d%BHS@lsyG+mSzd}77stt$1mfXa{lff!x!Zv9D3pfI}OWd8QXx1rBn7&T6sFk!^)84Z)pk_ z*B=+h{_3Hjpt)irLm*=B8}>)2VJa$iGzXTRIU_Pr?lm$hE*`?F@v`e$8b)tJ`sdlr~}lh#9C z7piZgpLD?6&z@X&8(2@?X_zH*-V1k|{sX>Zp4ik8`agQ>8NYl|ZNf{jjxzk+cGi@S ztxukokLMAR69bMI7iDBtMrh;s%xRXDE&4^gI>y|s`aQ#45-;`lXUDg{H?cF^_RP}p zc9eL8MRuIC-7^|req8i?LOP}9#3W;S?LRi>fhK7x9_d{3$MIqED|IcSw|VwgUwyT+ zaLPJplmF7u*H<5PZRzZc|JcSY$$aj~Yok6e=UG+654CupqBTDi&P>n+~A>%wW_U7a`jN&Ln>wImGu z{rD({m47XIxtr6?-&5PakMgJPJQJ^$sTHp5R{dVWbe30_T{t^S@1 zWl;{wRI6O*nd6_Y|LB}iqegXCUzKylza)HqUh;nf$?A2qZim6j)x`(428byE^;a9U zX0)N1z3>&yciGw>_6(!{h)+02i}>s(F#M}cVV{kyeLL!?qqfw&1rODO3Gn~a4ReJv zXaLQizth9(BEC~Ej|`Y)>k8^gyES2u|JMJl!c{EKC40~pe%g58Otk@>chdNU+T`9H z$7)<+#>X}$RWAR8VQ+?O|1_ZrFHW;aw@AAbzXc{scx(adY5ekYkx!&eb$u1}7V|CY z&v8xvoxU*g1at1MwboYiK7=Q);(I);X!kvMtkwzEHJoT<(>c)tGzZMxd0!UxnU~st zaY|v=?ne?YwAW)h&M`r+5f3!Tzoz`xcxgp@S=jr?Tp4GlNbI+>wLjtq!@u4+>lXZX z(0LErY3;+~<@&4j&RN=zT{B009DQf<{VCmZnn+FzJbHW>BRN>_Twtv{Yq{n6Plo@P zKeQifc)M`t;gVNRnhpdnblpv7`Ki3{4ChUM62A#A3jcr3@SoEvma!Ud&Ig4&k2H+3 zwV-X`@A!;%8sFnYJI$y1*|F>8spdCNpEzCB7cS(fmUhA%X0N3b@|1E?^&fjEBK5O$i50Nzxwgy`Ax*dBU<=&`%K!f)8R^(sq16RZw zl|TI94-JeJai^sCgtZgwqhCYq$7iHlan>&QXYJdj+DpHa&boT89!#)DYsf&&@k{>4 zZ;szxJi82W`i>3%>^xn4&eoj~v!b%p#(&J4I_V)9b?$c0g)@Kol=TNXZLjeQ^+|3% zWAew+9@Q$Z=@;#Bxm@MF9>X_#IDf9jW>FuhyT@@m`lWR% zd7`$HHk7vXjIEvw=WP3|&b@v;&o9O;%1j!4)YiWC@S?i+0cZ`-Kla>nPwvpA|H~QV z8w#@~|HTV-7M$?As&pD#1C4ywUBp%LU%a5`t0;H{_Xf2W-0|C1^Ser}9pgW1h<`q# zw#3f7QZQTlskyh^wCBC9^L|+SgH99iAHTVpc3#)iYxM7X(6}(|2vL~`lmd|qKkLcnSa|nZ9H(+ubz?a`ikZ)CTIzHedOYanLMoHHh0V@3(UxB>U|g2)VPK zzfm?|_i-R_zlzrUuOhz+dxN*%etSFjL^9sSKZX6(^lb89asA9UlmD7W-b46*xt=;^ z|JH-Owf1e_tPV_m#d`7YkM&v974H2G8K?a+GYoriy%}pmR+{+_Hpc3uoZiRrKGF1_iR%ACtLm~9v`Vo?m<+>) z-D*7BcN#Cnv`E9k*ZB6BUv<9D3o%~IEAm;v7oMJ%FUAoc=^MKI_GQfpM0s+yc%kd= zI{&JwJVm+s2>-+GT2S|yK)PGyF5MT#9l~GMI4EafZ6H|<3#_GffOQM)(SKyO(fPTJ z!g>?@!vhLa*=CzO9i@bVuaKdN9D+ zw+DL7%J8r6HTe?1yIjY${BHf*RLvhOwz+ZE&f8r)B|ZS}%rnr}c<-IscaoU=zi38P z{`5zx%Am4cvis{A1A5lRE@%rbQk!sJkC~r{4rKWMi}0V($oorGWlZ>@N%=Fs9Am;- z{wzPkX!6aeU+p|f^vUyU3VjZE!{~0`M;eOX=fF3@ zuz%K%f88DZpzcZ93j9k>+a3V)bgApC*JRx$_p@_$e;esEoCVjwIHJtn&N_$kFsef#)9=T8}%*5euhU@+^sftJQ@-zPwEcQP?tapd@L`QhWkYY#5g-jo{p5A$Eh|D?Vwf6?9R z&TYo)&fV^L)!EbHgJ*0`3|)-1${*(s`S1AmvUqv1t-KlCQumgg%jtkKPp8>O2n%{GWE(X@-AclsnhKzpa6m z+-BVxXCb1;*dBP~yV{1T{;%qC6}2OkJMX-+qI-+%+;jDH?VewST}@_O@tb{;SLCgPc& zzuWgl-0|HemlC%k9ny`m%k#$1o;`dGp3ME_cPV{gwWEKNEU|Hg-@X#t8ZQg+l~yb9 zKjie&PtWDQ`mq~I*I}HJb81%?cI_?zVb<=#K>z3bt5p8$c@@t7(RmV;ZMWT)v!5F^ z7iR0<#6O(<#{H^{|8Twv__w`Zci(;Y5ju-&a=HF$y*vJU@0Za!qgAXAr@w5u_s;wZ z+DCVzWkw^zsiy(`{aJQ>`1gFWI`sHnmsy(3S9IE8nBgBj=)B`_$>xdSjpyF9{baSi z_1|@y_~Nom{%7A+`o6v@Yu}Pv{C<6lm%rBcq;Wi)Y-B(l&!0VhYU7u_ui%+_^tvQo z>jX z#k`j0!J6p*3IhvN6t^Or+xkzr8UE$In)t!Se`M3Hd{*s>$^U!rz4v#*e_eaOSa&~I z^55|9bn|ca_uMXFGNTXm4y~;I^RU&l!@vKb6Izu2_n7`us!Pg}nk4;8cGKPt$$oSo z?5=vh@#HCC^bsh;)k@BsL~{x6dy8%t%(%4BqChIYmmiC_LMx9aEddLN&B zbM}6K+%nTwb{;38hv7AOud0tE?{YZad+e>gu=>By5_xjr&MzBZIQ}V9Yw)l6rxz}| z=%Vd~Q^UXT$^6v1GB*Ducf-Fh%U%}N_0$~y0sk1?8EI$#NRMLu19zzk_U&!_M>>$- zb?W#(s@}bi3#Y%U`7H3CVjEgh#|6!u4h8L~qs-(q_jvGv$1Sijpbq?d`q0}=fq%Jq z-OEO9b>1n;E$Zz2-)Q_o?E)Bm?$-HX;f!~!EvPl#+LRA8Hk*x6v4^r&oTm6szdwD> zz8U&ZUeU+O?KA<4hWji|>DlONVPiUm^{V>7owQ>*`LowUmVQx(F`eo<^zz0sqN85C z%c~|wpe6mQOZONN^T_gf?CQDUttIu%wWs(Oep#2yeT?YohJRtUy~aT~2fEF-zy0k- z^!vjOJFMZ{bI+~FD7Zjx(_Rt(XPj|HgYKAN{}+44xJQfuP}6_FztZL2Ver4%efQm0 zH~!~O(Rt73O;6;1M%OBOyIaI$f%|G2R`HA1b&}4C@jo-G*H|Zpohodk?y*d?)3n!T z|LQrbJL-_OY`|d?!ky<&53j1fy+Az9z6J1I{(*DGZ5aoiHgdVzr$rjCsy(mg`0ulS z!kfwW6}ZjlWPXm-a@mV_;zgOCxaIUKyLozU6SsKIc_HIHuMDj57X@-on|P+=pu8pW64> zpnYQvs-N;1D6D;}s4vl=vl3}1+DZO%=N`YW$aJoz-)x zE?h3VozghcxoJ9tX&wHl)2JJGK1JFSROb_nSCCs5ncRBb>Wwy_`(BJ;k2gK#^22r3 z;aStdQ~#JBMh#sOCcV7O&NQC<+R`xo#l>OtlMA)}dtrF6*X(f3f9p)dQ>KQQqifG6 zPaKZ^GsV&_%2Ir*m&xf-kDY#7)w9Z9D-Tbz>{os#Tvd8C_@P#uOZQ-G&E?7SHs3-$ z(AM7Hdz#LLdABS=YQBSio!JTgZTyq|uXLbw1>3cR`_+;ElG*K~`?t}4l}6nG(4aZn zD*Q|5QS?{zJySbA6MjXsj#wRV=)@K1|JCxpD&4$3o91VI;orO%&apP&dChAwegW@9Y`QMi z{}9e#m-~^r7y3w@U3IYbf9TxyD7PnsUi*#@H=ZQ?k7)+~rvD57Myp7tsxoGDa2mz@ ztHa^vCdQ55Q+DfffX9n=Ow*quJ);}6k7@Y#R;QKGz{=)n7WEaw#`H)Bx^>z46}6?3 zC+bsmQhPw(;F8@(gnPPAS{eM?o!G+vM#8Pl|7sq0b(!{ze@gdh!8$^0(D~1~ z{sZ{+s|eo}={^nWBip@4!aVxVYT#dd#QHbW|J62_{?nTNuj8LO?RVe=GcL!9`e{mU z>c{J(TvuKX<@hVY|KN+jKk0fs>6yAs-qX5{&#@d`ch#AH(pl*TpZ{yM0efoCzTu)s zi#d+YJH8yw-sU+oowir|t~TGp_1;hT|7#uiPh_*lb-Tcx=j&MWcc+`XyB*ywh9#*1lsT17qJ-ZY)+ymSVpbjtHHd2*iGf(u2n3&aO^cbjPEg4GoGCC5bm zZz{cn`%pQX(B{8{bsPVbUeiuMW_Z0f$NcKivHI|2>BP+I3XUqg-cT z`@)@G4(Cd4VZeE81B&{lZ9h-Efj>6ydAj2Ldhq{u(|?LI^Y}GzmHHJ}EXB>~Q}98P zcvZ5y6u;SgtLfM@|B1!7m(n0?*Ijf!kFX!NFrRVd%t;E01-9i3W>9kF0 zlhVqcQ#-9&+{^E{tqA{vFP>EuFHNKT?7Wwj0sLO{TiuC^+>!zB)FW{(*-dAxZwGI@ zpmkoluR?v~G|her(|f-O;ieY!|A_yoIsU7^Gk!Dac-hkQ{n^WF@4}|PTbxXGVk{^sGI6Y1Ouk+mRZ2ZT@|Aoy>gi#y+0sq3g z^?xx$S&JkYg~4CDR-lhRM@lUP&wDq4_A8PZzipRaz zZN(j&|Jh#rLx-C4AJX;zD<%KIUH=1h|3eAy{rz2a(VzM-Gb!}%n1yw>BwRTSdp|(( z|Ip^~&)T&Id(H`09HF!iQa%db7i0LqF7&FJ>ASza16hwAguI6r=)c*!+~^nd+-F?; z7B92e>VK{P|AqW_I>a*NG^$E3`dL|>MsC*%hac`~7=L!ZB%alk+*8~;KUdKwiJOLX zdJ;BwPr`axy6?%Ji{arS@d4xi%NN%_|CQl?Q>ibe|7eV!b9!w3r`iKM15q;F_CE^C z4X?fSTGjX`t$^+msEi#u*7m<>yoY_lzW(3F|COeV|47Hm&pvP6{8yC!U@fP;=n&~E zj6)MQ3H_^R?CwUt$QOnQd(Yx4OqBmiYOep}T*b*REe|gYS{NSfKR4XdWmdTD%o*W^ z6Q+f$kC_q%98&ORP)d3q{?<1?q7xux;4ayyN(xX^8d{6tvdHD|mW9k8d>wWxmlYyK;j|7rvB^$+Mj(tB+FOS*iUZ+zn$>}zk}{C}-= zt~~zu@Cb4%(j4^yg-G`yubDO_>b|Ah>nM|3U>+X8Aj25r zsr_9sUeteT^H=3y*7 z!qlic4`bzvc}5xK<(3UEm&#)8gW6Q|gVy3->wj$i3tWPK=`6PXO|swaKoVBlsn6R+ z=QT9$v(G+0_KCi4KkegM<@)Qdujp=!hU1Pqj=SF*g>$?6NOFHQ89V<+$*re6^7U`6 z!oM(f=Xou z>gCSLmSU}sc;}{*r#CPEUtDhZuaf^+dZgpV^2oov1|Gc(HSGQeO_u(LX+<66>11t3 z{w~_*NPA9WfCr%sQb3@o9JkR|w3H{vv-@$(kmasKy?5TWPZN<{L@7ubaX(QeWqaS-y zdzBW1d%I5yH=h1RxawGq^&Bp}M|+k{kD_du-eTWvI@Bb!8(RPH*B0ks~uhuwl4>0Q*n&4kNvWj@7qIRRAanc5j`EXZcTkeigns)w` z(&bKps{LQt`iHvKKci!q{Fm%=J(|8zYOD<`xw(IHalC)!;gFfu|9uGk=i@u8nRbG` z#_TcUOz35c-m^O%-*{n(-2qAe;_<8IhX=aP34_j>X?oOEznfyZ89dR__|GKCf7%z? z3IFcMM`e0V%IO!wRb$xk6>;Zs1s;lgl;hcCPR3h?`SdyRgJIM8K^RT3?S9qyrDZA7 z^DwEM@&&v&^vc%h|K9&a{tKhF{zq6g`7g}d`UkZKoM*+4JK!6%5174PrvC`**cF}m z(;&USfpJpyztC<-_UGrnDgE^o-}gUm#`=eP$3MC2?Z#83EA0Hb=KNmQ8d%JT%q~B> zf6I1P#g8Tl_uJQXXV8c+{NDK=+y0bVZA96PHK6Y-|4`>GMm=iED{tvsr={WfLGyJV z@k~2Qox3<{{oZ(PNw{2ljiFVfQ)27;nz&{3?6!y2Vu=5(eL%+2PB>bKhX zkNT|bg?$_Um)_E-vw|A5XVS)hB*P5;xZpQ0$;j5YS0_EYG576@_f87a$=(U)#{K;r z8@J)Q&+%p5nLW409!2`SQC7dT^nK$G&X=>ZBqYy2?jQNQ_x17{o5iN=UH`=mi(8+BeT`V#xb?Cy7sC7J#M{$+Cgzh3zd{;xl2oME1^@y5r z4@>60YvV!hN}pkT2p#8t<2O$D?|axt(aOIqr3G$!-(I%N1!i8dHi7Y|rkG5ye~|j? zGR_k`+k1xn0SC&jIX)=!O!>XJcqR81^B}C!HT}O8$d(=Z2kOBxem}uzReZP3W;A-lZ@p;e*GxVj_b=S>Rn0v!*ETtTuWA$V?`|o+9sjxA z68;k$ds^6Odj6ccd)oc8u&G}f-uMuna{RYq{NKkvk^jO!>mQK+tk=)i{|nQcXV|DU zZ9e{)vw&pWD#A5ue=5j-^d#|!?f-K8i=RyY0sk_-|AqCRb>W}>?_KB5)|hszBmee~ z-0nD8eatBu|L1Prrt+Kf-|oG5j>fx1YZ=PZZ~w`m?|w3}DbJOMPYG8aJvCf^?6h#p zDKo<0i)M$%2F?$|?pUn7EpOWS+%rZm3kzqy6W-Q29kdmuN2$H|DENc^?>~;4Ze@$~ zsm57v?H{0T;O!%G5cH4xBhO3tPw89sE3inL;HD`LrPz&eVm@iw>2sQoyXS7?lkrmS zmX#Uz0V{$3O#k_k^!1I!1Kdw%=RXL$t82W6^K0!6RPKObd|&sw*jk^Q0pf$X?Y#5O zoDb9}_GzoQ?0;EJ>8+u(?Cy8)ui&o#u>Y%7`0w8_sUzyEY|{Em{ctb8d)R}f#(KwJ zZdV?zv*Mb=zxt#13IAoJR&*EEstDODC8^$MD-uSa-qL1$AyZTRK!!2h`v@;!_ zzjcx2H)(imFP6@G-_D8Syy%tDqb6zm^M>PRDDU)}^^MBEpLn+0?z*!n(;wjj_`$#D z@|Ni2^m04kc*YCZ3S-!4z5XM;#pb_+ z%l!OX$$mTkT-XKw?HJFYU)!~7*9MLAG>d=jk!cVQG&29IA9qKxf6Vs(iU(}|E0h1N z8vj|4$^Vw{ANfC{FSK*x^vY=M^v~((;jk43`P~x!k^dg|@&mzr#J=G_O)E>!>lt2f z>|L<^E1}DdI>&Nvows%HlyJ>4ngbAKZ$D#J7~Ex!?Q?wm@3W+b%@5;;Ee*5Ay&V?K zjCvIP-j>>l9RL2kW`qB!q1Qf={X4y8_($*QvFBLhQNAa~KH-zN)!!BQ$Z4B@JMobt z**9Yo&*EBIG5_4(^YDB;Uj82G>ftRd`Q_=Q@zZCe(^~ysm^SiURucUq_Q;o&mBDw?^U<6rXskrnAbk~s&+AahLa5I5z6ByYLn z=X%G5JHBE%cXweg;%us=(@*JPNVl)X?z`!J-YT1Dh3vm{H_3kC9@BLf?W<8f_*s}NT=7i27}vshICrz~9;R&b{TYV& ze&U)@xF+!utb5wE!V}NSXyLLvOX)p!<$_kNe{=aSIlZy4zkx7{{-pHnw9^YL;c(>i^6TE&ys#WJzR72v@r09X_Dc>u;W+! zVC`{MSLBcE2Ybl;3uP_=dk0La#zj^J3$M66_RtxkJE)(!{i~AMx(iye+sdx=Z#!*P zm^JpDn!`&u{^3jiF4;Zze#7*B%Fsn^0Po<*>C@lyH##QRO~Rz<70=cAczTg{s{M*M zMg0sA?V@`cc3Q~YtvrceR=$Eyoqw>Wgx2CelmDjwOW)a0a(Z3Kc&R;WvUia)Jln~% z5uVxC%lTH#=|2tl>+U;S|07-?o}K@uG(RKk+x#!%pK@pYTiyHLqx@IDQ}_gHIc=!3 zgGDRZ1>NIwY|k^=Hi;A6(>Pu?R~|MsymZf!8sqD6-P@b<-?wp{sjs~i#%K)#@_49Z z&fqR{!=N)~g&R+p8Ls`q^l-(YT7xEjg)e&UHC~u|-SBK!274LaQadk?%G*!;ap`WO z46EQ3{y~QKKVWLO<>Z-K|Iqv`wF`2p`Wa7fJ`nO>?MGXsW#>Q3w*IepgY(b*{KF2KN2q)K zXBGZ)S|l>!pvVIQay#;X)2lh#>ME&6r{T~m7TEl2O#z!ZqJuMT!}Y5Im+BOK{o>z_Fdn#Q=MmNh z^aKB=%vv%2kri2bm+htVSHyo{zpK*gz3+q^J5C$;B!AB67_nT)m&@fIO(s_?{c1P- zV?7k!F~p*H5H#5zp#hxVim|*2-#G`dl5}%je$$4@2A5;9vSb=YM`l*xXq9 z4wn|KEo1tRaKEa~{%ObAKl|>xuigKsuvPfiJua(gU4-3zr!*S0SH4km!FK+k`VX`r zw*FZ>wZ3@E=YMVdr{3|;1ygnIc&E8J{`2q6>qNA*b~U3h&t<>#J%3hRrt#zsy$2pX zU3&iF6}AB_@j@*>c;kJ|D^Nd+XTKljO?o#>d+lxMQOit^qV2x-;<@3Dvu1~zPMl?Y z!J3?lcN}55QY$m^Dz$6H zPwAgN*9!Ylx+$+`ZdjzdmTRvKDc1@6A9WAicEa=*h0V_k_Zx@@)|LUYtleK#`g}XB z@o5wvG;sd2?!m4|&MQjsn>=}Pg|!jt8*o0Zt$))q``>N+Uwpy*m*HReSpT=R<3EP~ z14}q}+NJg34JX+4ym%kRqP#^Yy&5sxf09{4zyGFW?|w!-u%-E;Ql0_jm|d(fjs z4Ou2VYLV8w%?h`lJ6ZRLXzfPNFZ`oFG2V)Nb{eJM?eucn!}xn@ zyI&ID+lVU;j_p_zdY9v*@9A@%kNQwq`d1vJG;{aJr{-6Vr}t-XTBP&;>&E{xTEFuv zA^8i^YwZ4a=|F2s*M|jIyRfRB+p?yIaZfc0mE(^;9$lyEhnwVlMKa#bzh%9&0@(Ub z#k2cggnK*x6a3TvZO!`6DE}FEpO(|a=%Me;>FxBvP4vRu4Po6I#OnPZ(dYMq0eVTp0KGafcrB0qpU5tC=+}lZHwDA2&18 zoyLZXg?mi5-Lzgq{b2vTlpZ<0GJ2+d1@5ZpS-3|$`n#v^?)j`>lK zSzTIsUSG9r(l}&#c=l$s+jZh!_wNqr)TtA~oU^Y!D-1J6Z)cxqy@U0CrN_6?e3t3| z*IaW=gX*P9|Ji%*y;qTLRKc(Yj{RRs$HqUEuAP4+J<7&E#UGz_{I_QRSFZmU{)>8W z8aZv8o@G1AqKuX$-taU$&gJsQ@Q!`;Q8U7wXU_?bU7<5L|G6}beL`n*zVf#51Uk&S zTBjV>fRBS)%btC>pu^TQKYFEW~=tDv*cZ)To=b)c-F^R+#n1UIl|Y1r*c z)Q8bpLCJfQ{o(=oyanx`S%InaTR)2DTz2Pm(_B5J?y)@J*Lkd-vc!6falMXS8N9T{ z*mZ5{>)xWfgzKLOtD8tRb2giueT(d8?u)r!t#@d{nLc~%wHIUicFwbGhyN-{*`Bx9 zVhhdywfirW7WW0Ls_`S+{|o-n|J5g8{g2)M!v3#%$A6;ypQkzcmhc|wdF3G~T~|cU z>aZ)KIW(pWW=z(_^1vI{{eEV+>4e#~hJky9|>W>}vMnN!bCvisIQ{_&4L6eex$kMO&Wu*%uzcJ7JP7RK@0s2ylL^2j4O z-?P@-7x;Jg2I0I>xVP~i^(E~7U-5&@e<^K0|H{XI>e~O9WmsmdsImp|MhQ{+0c~SI=tf3Mv2=e_#5x-8Q*ZZ{*0eR6r-qt?@> zPrpKAm*0~7x3M3|?0oJ^SYK7TPh0Uo8`gem45&e-lI`~b^Y%O9h$FB~_t{SO!_!yJ z=$%$dS(2PhOhY? zX`?%BU4J_&Tqa%^VC}^Wl|7aP8FkeW(xZMiQ~gx*sJY?6 zZVNPSEj>!QOVp!cdjY1|H-KKle&JgDfvkgni2L_rbw~J~Itxd6!++g(*ZMY<33^mx zrJzfsRq+kEd@z0|9Db>P5!Y~4+V)~Y6P5 zLd$XmT27cSp{v&RY$myEV?XFAjQwcLhII>!``fwaTJvmkU*Z?~zE%BSV*=rybwTuh zHE+!Q7bgG3Lux+S=l5Tr|A;pjYijlQ&pn;ce+n8qMr+e;W+E$!IFbGG=~3JVh?z6-?eMDZf?-YSwMWkieKa?Q-VgrSqtkm|-RTPU6O+=w zpKH-DbxWVU9RRzA#RUIOi|X)2{&{|ilZN5hjfcTb!?=6d&EKKtEho(hGe*{47g7WN zAC4I_<}CG5zaW_mE*YQ9&;69#X3aC}9_-#P)_%V6#v9GzpEf}J)1W?sl4-Q_&()vE z*FQ@yvh~lhef+Z(>mQ>02P0P=9I;){Yo_X7;dF>026+pZe}kpL48XAN?c8o~2)1zp?B|nag#p@3i*9cw%O_<|wVV`){o+`U`s0 zT$>9VbdLHQf7X7219h*l)=4mDeA%A5CseqH|9b49HAb34j(JsM$;z%gi|;Au6Tcr? zK%X*=itniMZ-!2u{>s{AI>oZZa(SAuu0>19#=Fm-r!^?`-LL$?2Olhb_0?C87xvNp z?c6J2el6+locp#ay1&*jw~?;Gc#g06Y%=!m@*n)~y6djC{~g{@JCN^x5s!Vw^&jzw z^?%WSS~LEG{&U4av7Wq6BRz1VUci(4yY2N={chp%`i(enIy&z-M%=wjZ*$N1hq0B* zq<4UO4A1nDbN}jUNx3X>Bm=aBe#4IWu6;O>EziGP_^tJ3PGS zW44D)ZQ}7>j_>6=U`80yxBj&cI?_PFx*MP(n>heG{|EUm%(HLU#y_P`+4-NeGvX(k|7|_~=STc!byFp`6Wm9P zIo?-bla{BaGzK0rt9UQ@<#=&-w-v9?eo^m0_R;lEyc>kcL1qR6|HU-r%A^pP)yYw#?rU3IwDvi^>?XLh*Z&$FeM%?Y=h zI49hC^4xISsao%?$!If*qJf^G{@;c@zuRoH z4R=~Q4`kxLurz&=)Swww`mPsnU_-9`1j=a7w)e*bXNGsv2((mXUz)_ zb=Mlbfs57eSQ=ite_0s&`0_C6#ka%s*Wa;q7tC=inEsyiyE)U&jK+4~ToBi9EYli} zWqOW==YQX@E(FtjS|7A(3{7i)?$g{Chxf4NpLyT4WUnsyzp4!9*V>)$S|8WI+8&Mb zR5S)!06%*=_Sj=rQ5(Yg2gW}2*w82$Z|DE$*~b6Xhge&D!2K`Q|CN8M&c9{+r;Ptx zwmBYBJH=7zo;^G6%65v&GF}Wb#Lv=p4E5dz*+14WSm@m18S$s5pQn|?bw5uh4P$O; zxy#>^y(8m$?XA6H(tpgT9KH9MXk|>(_h+Y>>ycLg`BNV*487BKglpE|A!j;AD}+%rh+khzSa?T*OXaJ>lfN_ zexdGiY$HrJo_OMk4KrrU;NHiIOcVTP4)hCXBQT9>N7`yUhq za{Z?@^IwB6SfF|>Fy*-Mx*aII__Nh>^6u_Uho~%JDDU!-E+3(aVtLVP#U;!2b2WpRM)R3&MzpmxbAHFkk(_O6|4( zBr*F@=Jo#j@6Q`GYSeMk?YZ-MePQ!6(t*~HzSCak7q-b@lM^+ zeq#n=GCigx+rkpA#8VdE(yC71@#ATNquzT@GX0~QbP(_Jy1(01Jlj(FGQXzZ)wCS3 zEVE4i>7n(|j(zOQ_RyTe0b0Xdg9g_hGdnzR=|ashEH|Bnwb&-lKc#YfWqCs5E~B;n zY)4^|`xw^~&esxNZNDRPw;IQ9BN@&;?hQKMyfSOntd_7315`9uQ2E0j{$TeXDJ{+m zWf9S8;GgjyH7($u`vs8yt)Bm_CI4&HF>Y6348zU~DTdN#_s7OPHK||I=l;SNvLF2$ z4EEb^N(pnGkK0YtD3`yw%q^G6WdB|hEPu*?-fy;^drK#}Qv4aeN9l;iZ$D*TcxCW1 zog?vn)<^tw>Mkpbd5?$gvvA?Uo6kP`>~9K(>kFG2o>_Yh=4v~1=)hTrZMDw7jrzSN z|8KqZ*5=0i!M**2Y3}~5us=Z0w*Ot}+WvRx{Pcg>Q?MTDm3SZ@|EypCd0+a!%m3El zsf@Asp5olob2rDG@Guc~&H=abk*O19AyA^4~c44;o z8aJH5ka zPx5{(;othbtas2_pEg@>y|vwSAzi4UfB*i5e~lM8+-untrG;q_-VOi4JNo}>>RWLC z*V^g>*!_=;|9k(pHTb{xf`z94Q~z#KdF^fy9}y#|fA#YmrQxQ)7-2kqb$YZX9uDkH zmFypH_y=oI--!N2xFW4I@6 zAHbjVAs*?q$kuAJkKjMQ5Z-$0ttW1|<(4f4V(_>QWB%H&V&^ZVOz18b~1{?kh>^nXkTk$<& z+TE}{9C)vOcYG4Z!==wn@l1ShX0leg4?NIkALg0hkEZDDff=U?O#)9|uH_i|kF z>a#EBqePtd6wU`|yq<9M$KgkGA=Vz<+jXwZhq89)(>hl3QSvOZU+eVG)E>r7g#Y!V zpMOSUJ*#V;cU7Hl)lTdB+o&yQ)R~BkcQ)wEXU9HvExB*&pXFD1<&{?|I``S`|6>1_ z+5#K@R~x|nU-Yl-{x7|o{@?oXf8>9G8L&0*kcesOzKpf#UtlPGZw_b9BQd>dJQE-6 zp?82Wu#b$rLUW=XFQ!`^FP5>|&BH}(m;H)wiQ)V`e(QjatTUQm`P0|!r?$w;OS?f^ zK-q3Sb-LDFzm<$tew0Z1bfzWQ|CsLSV!s!L=#%$W?jORwMFAUq5 zPEF949O)mUC#fCa4uD1(&c8+e+xj2n^%=DXw*E(MD}{gbpMi#d$BWwqJ{(uI?UWbh z=JMLHQ#%fMkbcYyISb~&-DT20QtZVrUKY2#j9@oyf1-QLyGTFgmBvlWJjeJCd|$T5 z1T&03(eEH{{|?q7aQ^Pde{qIgefwZPooH@dsins(&_4CQYFwW&&vk^uwKONSy2dIw z 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 I+Vs``0KQHqH2?qr literal 59403 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 DfNejV From eb102479f4ee1548803fa6e03f46fb08e2529053 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Sep 2024 02:47:19 +0900 Subject: [PATCH 191/308] Use beatmap icon for `.osr` and `.osk` for now --- osu.Desktop/Windows/WindowsAssociationManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/Windows/WindowsAssociationManager.cs b/osu.Desktop/Windows/WindowsAssociationManager.cs index 92cffd0987..c8066cabda 100644 --- a/osu.Desktop/Windows/WindowsAssociationManager.cs +++ b/osu.Desktop/Windows/WindowsAssociationManager.cs @@ -42,8 +42,8 @@ namespace osu.Desktop.Windows { new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Beatmap), - new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer), - new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer), + new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Beatmap), + new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Beatmap), }; private static readonly UriAssociation[] uri_associations = From 385eb5eed5a4842e0eeb4e4b4dc450d77ec7c407 Mon Sep 17 00:00:00 2001 From: kongehund <63306696+kongehund@users.noreply.github.com> Date: Sat, 14 Sep 2024 16:32:51 +0200 Subject: [PATCH 192/308] Rewrite GetConvexHull --- osu.Game/Utils/GeometryUtils.cs | 76 +++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index d4c1dc2db7..4c90421aca 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -152,42 +152,72 @@ namespace osu.Game.Utils /// The points to calculate a convex hull. public static List GetConvexHull(IEnumerable points) { - List p = points.ToList(); + // Naming convention implies positive y upwards. - if (p.Count < 3) - return p; + bool isCCW(Vector2 a, Vector2 b, Vector2 c) => crossProduct(b - a, c - a) > 0; - p.Sort((a, b) => a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X)); + float crossProduct(Vector2 v1, Vector2 v2) => v1.X * v2.Y - v1.Y * v2.X; - List upper = new List(); - List lower = new List(); + var pointsList = points.ToList(); - // Build the lower hull - for (int i = 0; i < p.Count; i++) + pointsList.Sort(delegate (Vector2 point1, Vector2 point2) { - while (lower.Count >= 2 && cross(lower[^2], lower[^1], p[i]) <= 0) - lower.RemoveAt(lower.Count - 1); + if (point1.X == point2.X) + return point1.Y.CompareTo(point2.Y); + return point1.X.CompareTo(point2.X); + }); - lower.Add(p[i]); + if (pointsList.Count < 3) + return pointsList; + + var convexHullUpper = new List + { + pointsList[0], + pointsList[1] + }; + var convexHullLower = new List + { + pointsList[pointsList.Count - 1], + pointsList[pointsList.Count - 2] + }; + + for (int i_points = 2; i_points < pointsList.Count; i_points++) + { + Vector2 c = pointsList[i_points]; + for (int i_hull = convexHullUpper.Count - 1; i_hull > 0; i_hull--) + { + Vector2 a = convexHullUpper[^2]; + Vector2 b = convexHullUpper[^1]; + if (isCCW(a, b, c)) + convexHullUpper.Remove(b); + else + break; + } + convexHullUpper.Add(c); } - // Build the upper hull - for (int i = p.Count - 1; i >= 0; i--) + for (int i_points = pointsList.Count - 3; i_points >= 0; i_points--) { - while (upper.Count >= 2 && cross(upper[^2], upper[^1], p[i]) <= 0) - upper.RemoveAt(upper.Count - 1); - - upper.Add(p[i]); + Vector2 c = pointsList[i_points]; + for (int i_hull = convexHullLower.Count - 1; i_hull > 0; i_hull--) + { + Vector2 a = convexHullLower[^2]; + Vector2 b = convexHullLower[^1]; + if (isCCW(a, b, c)) + convexHullLower.Remove(b); + else + break; + } + convexHullLower.Add(c); } - // Remove the last point of each half because it's a duplicate of the first point of the other half - lower.RemoveAt(lower.Count - 1); - upper.RemoveAt(upper.Count - 1); + convexHullUpper.RemoveAt(convexHullUpper.Count - 1); + convexHullLower.RemoveAt(convexHullLower.Count - 1); - lower.AddRange(upper); - return lower; + convexHullUpper.AddRange(convexHullLower); + var convexHull = convexHullUpper; - float cross(Vector2 o, Vector2 a, Vector2 b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); + return convexHull; } public static List GetConvexHull(IEnumerable hitObjects) => From 30096c1c7198168c667f85873a60461410df296a Mon Sep 17 00:00:00 2001 From: OliBomby Date: Sat, 14 Sep 2024 17:26:04 +0200 Subject: [PATCH 193/308] clean up code --- osu.Game/Utils/GeometryUtils.cs | 74 ++++++++++++--------------------- 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 4c90421aca..8572ac6609 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -152,72 +152,52 @@ namespace osu.Game.Utils /// The points to calculate a convex hull. public static List GetConvexHull(IEnumerable points) { - // Naming convention implies positive y upwards. - - bool isCCW(Vector2 a, Vector2 b, Vector2 c) => crossProduct(b - a, c - a) > 0; - - float crossProduct(Vector2 v1, Vector2 v2) => v1.X * v2.Y - v1.Y * v2.X; - - var pointsList = points.ToList(); - - pointsList.Sort(delegate (Vector2 point1, Vector2 point2) - { - if (point1.X == point2.X) - return point1.Y.CompareTo(point2.Y); - return point1.X.CompareTo(point2.X); - }); + var pointsList = points.OrderBy(p => p.X).ThenBy(p => p.Y).ToList(); if (pointsList.Count < 3) return pointsList; - var convexHullUpper = new List + var convexHullLower = new List { pointsList[0], pointsList[1] }; - var convexHullLower = new List + var convexHullUpper = new List { - pointsList[pointsList.Count - 1], - pointsList[pointsList.Count - 2] + pointsList[^1], + pointsList[^2] }; - for (int i_points = 2; i_points < pointsList.Count; i_points++) + // Build the lower hull. + for (int i = 2; i < pointsList.Count; i++) { - Vector2 c = pointsList[i_points]; - for (int i_hull = convexHullUpper.Count - 1; i_hull > 0; i_hull--) - { - Vector2 a = convexHullUpper[^2]; - Vector2 b = convexHullUpper[^1]; - if (isCCW(a, b, c)) - convexHullUpper.Remove(b); - else - break; - } - convexHullUpper.Add(c); - } + Vector2 c = pointsList[i]; + while (convexHullLower.Count > 1 && isClockwise(convexHullLower[^2], convexHullLower[^1], c)) + convexHullLower.RemoveAt(convexHullLower.Count - 1); - for (int i_points = pointsList.Count - 3; i_points >= 0; i_points--) - { - Vector2 c = pointsList[i_points]; - for (int i_hull = convexHullLower.Count - 1; i_hull > 0; i_hull--) - { - Vector2 a = convexHullLower[^2]; - Vector2 b = convexHullLower[^1]; - if (isCCW(a, b, c)) - convexHullLower.Remove(b); - else - break; - } convexHullLower.Add(c); } - convexHullUpper.RemoveAt(convexHullUpper.Count - 1); + // Build the upper hull. + for (int i = pointsList.Count - 3; i >= 0; i--) + { + Vector2 c = pointsList[i]; + while (convexHullUpper.Count > 1 && isClockwise(convexHullUpper[^2], convexHullUpper[^1], c)) + convexHullUpper.RemoveAt(convexHullUpper.Count - 1); + + convexHullUpper.Add(c); + } + convexHullLower.RemoveAt(convexHullLower.Count - 1); + convexHullUpper.RemoveAt(convexHullUpper.Count - 1); - convexHullUpper.AddRange(convexHullLower); - var convexHull = convexHullUpper; + convexHullLower.AddRange(convexHullUpper); - return convexHull; + return convexHullLower; + + float crossProduct(Vector2 v1, Vector2 v2) => v1.X * v2.Y - v1.Y * v2.X; + + bool isClockwise(Vector2 a, Vector2 b, Vector2 c) => crossProduct(b - a, c - a) >= 0; } public static List GetConvexHull(IEnumerable hitObjects) => From d34e8ea69e78f80940b2cade87b9c0dba6f066b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Sep 2024 16:15:09 +0900 Subject: [PATCH 194/308] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index d5bdfd91b5..c7ce707562 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index da1cec395f..bb20125282 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 785a7255074bb374ccf92b3d6ec68d5c7a3b6117 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 15:12:02 +0900 Subject: [PATCH 195/308] Fix osu!catch fruit rotation being applied too late --- .../Objects/Drawables/DrawableFruit.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 52c53523e6..7bac6b588e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; @@ -28,11 +27,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables _ => new FruitPiece()); } - protected override void UpdateInitialTransforms() + protected override void OnApply() { - base.UpdateInitialTransforms(); - - ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); + base.OnApply(); + ScalingContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; } } } From a99dbfa768e56bcad9e6e8ebff295d42f482a4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 08:21:58 +0200 Subject: [PATCH 196/308] Add failing test step demonstrating incorrect end drag marker position --- .../Editor/TestSceneSliderSelectionBlueprint.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index c2589f11ef..fa8db51e09 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -218,6 +218,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("tail positioned correctly", () => Precision.AlmostEquals(blueprint.TailOverlay.CirclePiece.ScreenSpaceDrawQuad.Centre, drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre)); + + AddAssert("end drag marker positioned correctly", + () => Precision.AlmostEquals(blueprint.TailOverlay.EndDragMarker!.ToScreenSpace(blueprint.TailOverlay.EndDragMarker.OriginPosition), drawableObject.TailCircle.ScreenSpaceDrawQuad.Centre, 2)); } private void moveMouseToControlPoint(int index) From 3e63fe399f75f31d35803564e40559f59d4a213a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 08:22:37 +0200 Subject: [PATCH 197/308] Enable NRT in test scene --- .../Editor/TestSceneSliderSelectionBlueprint.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index fa8db51e09..f0f969b15b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -22,9 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene { - private Slider slider; - private DrawableSlider drawableObject; - private TestSliderBlueprint blueprint; + private Slider slider = null!; + private DrawableSlider drawableObject = null!; + private TestSliderBlueprint blueprint = null!; [SetUp] public void Setup() => Schedule(() => @@ -233,14 +231,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } private void checkControlPointSelected(int index, bool selected) - => AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected); + => AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser!.Pieces[index].IsSelected.Value == selected); private partial class TestSliderBlueprint : SliderSelectionBlueprint { public new SliderBodyPiece BodyPiece => base.BodyPiece; public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay; public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay; - public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser; + public new PathControlPointVisualiser? ControlPointVisualiser => base.ControlPointVisualiser; public TestSliderBlueprint(Slider slider) : base(slider) From 67a7f608f155aa7c1e5abcd4f17c31ef23028f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 08:23:46 +0200 Subject: [PATCH 198/308] Fix slider end drag marker being in incorrect position for stacked sliders Closes https://github.com/ppy/osu/issues/29884. --- .../Edit/Blueprints/Sliders/SliderCircleOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs index 247ceb4078..9c2998466a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleOverlay.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (endDragMarkerContainer != null) { - endDragMarkerContainer.Position = circle.Position; + endDragMarkerContainer.Position = circle.Position + slider.StackOffset; endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f; var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f); endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X)); From 2ccdad41e793d9bb982eabeedf6f5be4a08367b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 15:27:16 +0900 Subject: [PATCH 199/308] Also fix banana showers --- .../Objects/Drawables/DrawableBanana.cs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 26e304cf3f..9a4bc45bda 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; -using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -36,23 +38,37 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables StartTimeBindable.BindValueChanged(_ => UpdateComboColour()); } - protected override void UpdateInitialTransforms() + private float startScale; + private float endScale; + + private float startAngle; + private float endAngle; + + protected override void OnApply() { - base.UpdateInitialTransforms(); + base.OnApply(); const float end_scale = 0.6f; const float random_scale_range = 1.6f; - ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) - .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); + startScale = end_scale + random_scale_range * RandomSingle(3); + endScale = end_scale; - ScalingContainer.RotateTo(getRandomAngle(1)) - .Then() - .RotateTo(getRandomAngle(2), HitObject.TimePreempt); + startAngle = getRandomAngle(1); + endAngle = getRandomAngle(2); float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1); } + protected override void Update() + { + base.Update(); + + double preemptProgress = Math.Min(1, (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt); + ScalingContainer.Scale = new Vector2(HitObject.Scale * (float)Interpolation.Lerp(startScale, endScale, preemptProgress)); + ScalingContainer.Rotation = (float)Interpolation.Lerp(startAngle, endAngle, preemptProgress); + } + public override void PlaySamples() { base.PlaySamples(); From 3f4422429da16292cfc8a8b48797be1197507393 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 15:37:54 +0900 Subject: [PATCH 200/308] *Also* fix droplets --- .../Objects/Drawables/DrawableDroplet.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 8f32cdcc31..c92fd7cbba 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; @@ -28,15 +28,22 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables _ => new DropletPiece()); } - protected override void UpdateInitialTransforms() + private float startRotation; + + protected override void OnApply() { - base.UpdateInitialTransforms(); + base.OnApply(); // roughly matches osu-stable - float startRotation = RandomSingle(1) * 20; - double duration = HitObject.TimePreempt + 2000; + startRotation = RandomSingle(1) * 20; + } - ScalingContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration); + protected override void Update() + { + base.Update(); + + double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / (HitObject.TimePreempt + 2000); + ScalingContainer.Rotation = (float)Interpolation.Lerp(startRotation, startRotation + 720, preemptProgress); } } } From c1c0d49bfeecba12db92f48d8f3c586ab820c3e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 16:15:45 +0900 Subject: [PATCH 201/308] Add comments and fix bananas stopping still if not caught --- .../Objects/Drawables/DrawableBanana.cs | 7 ++++++- .../Objects/Drawables/DrawableDroplet.cs | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 9a4bc45bda..f6ecdce616 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -64,7 +64,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.Update(); - double preemptProgress = Math.Min(1, (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt); + double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / HitObject.TimePreempt; + + // Clamp scale and rotation at the point of bananas being caught, else let them freely extrapolate. + if (Result.IsHit) + preemptProgress = Math.Min(1, preemptProgress); + ScalingContainer.Scale = new Vector2(HitObject.Scale * (float)Interpolation.Lerp(startScale, endScale, preemptProgress)); ScalingContainer.Rotation = (float)Interpolation.Lerp(startAngle, endAngle, preemptProgress); } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index c92fd7cbba..73442a502b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.Update(); + // No clamping for droplets. They should be considered indefinitely spinning regardless of time. + // They also never end up on the plate, so they shouldn't stop spinning when caught. double preemptProgress = (Time.Current - (HitObject.StartTime - InitialLifetimeOffset)) / (HitObject.TimePreempt + 2000); ScalingContainer.Rotation = (float)Interpolation.Lerp(startRotation, startRotation + 720, preemptProgress); } From f8fff4074ddecafbd79076662a11df71b7cc7610 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Sep 2024 16:18:29 +0900 Subject: [PATCH 202/308] Fix rotation not being updated correctly on start time change --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs | 5 +++-- .../Objects/Drawables/DrawableDroplet.cs | 6 +++--- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 6 ++++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index f6ecdce616..10e483b577 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -44,10 +44,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private float startAngle; private float endAngle; - protected override void OnApply() + protected override void UpdateInitialTransforms() { - base.OnApply(); + base.UpdateInitialTransforms(); + // Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms(). const float end_scale = 0.6f; const float random_scale_range = 1.6f; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 73442a502b..fadd630116 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -30,11 +30,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private float startRotation; - protected override void OnApply() + protected override void UpdateInitialTransforms() { - base.OnApply(); + base.UpdateInitialTransforms(); - // roughly matches osu-stable + // Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms(). startRotation = RandomSingle(1) * 20; } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 7bac6b588e..877fae9d67 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -27,9 +27,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables _ => new FruitPiece()); } - protected override void OnApply() + protected override void UpdateInitialTransforms() { - base.OnApply(); + base.UpdateInitialTransforms(); + + // Important to have this in UpdateInitialTransforms() to it is re-triggered by RefreshStateTransforms(). ScalingContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; } } From 1b17231da47daba7743d868e7ba8f6bb281a3b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 11:35:16 +0200 Subject: [PATCH 203/308] Implement "form" slider bar control --- .../UserInterface/TestSceneFormControls.cs | 14 + .../Graphics/UserInterface/OsuSliderBar.cs | 6 +- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 330 ++++++++++++++++++ .../Graphics/UserInterfaceV2/FormTextBox.cs | 2 +- 4 files changed, 348 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index eb8a8b3fe9..6dd7275abf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -68,6 +70,18 @@ namespace osu.Game.Tests.Visual.UserInterface HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, Current = { Disabled = true }, }, + new FormSliderBar + { + Caption = BeatmapsetsStrings.ShowStatsDrain, + HintText = EditorSetupStrings.DrainRateDescription, + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + } + }, }, }, } diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 9cb6356cab..334fe343ae 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -46,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface protected override void LoadComplete() { base.LoadComplete(); - CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true); + CurrentNumber.BindValueChanged(current => TooltipText = GetDisplayableValue(current.NewValue), true); } protected override void OnUserChange(T value) @@ -55,7 +55,7 @@ namespace osu.Game.Graphics.UserInterface playSample(value); - TooltipText = getTooltipText(value); + TooltipText = GetDisplayableValue(value); } private void playSample(T value) @@ -83,7 +83,7 @@ namespace osu.Game.Graphics.UserInterface channel.Play(); } - private LocalisableString getTooltipText(T value) + public LocalisableString GetDisplayableValue(T value) { if (CurrentNumber.IsInteger) return int.CreateTruncating(value).ToString("N0"); diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs new file mode 100644 index 0000000000..91ce9da2d2 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -0,0 +1,330 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Globalization; +using System.Numerics; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormSliderBar : CompositeDrawable, IHasCurrentValue + where T : struct, INumber, IMinMaxValue + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private bool instantaneous; + + ///

+ /// Whether changes to the slider should instantaneously transfer to the text box (and vice versa). + /// If , the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end. + /// + public bool Instantaneous + { + get => instantaneous; + set + { + instantaneous = value; + slider.TransferValueOnCommit = !instantaneous; + } + } + + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + + public LocalisableString Caption { get; init; } + public LocalisableString HintText { get; init; } + + private Box background = null!; + private Box flashLayer = null!; + private FormTextBox.InnerTextBox textBox = null!; + private Slider slider = null!; + private FormFieldCaption caption = null!; + private IFocusManager focusManager = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Transparent, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + textBox = new FormNumberBox.InnerNumberBox + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Width = 0.5f, + CommitOnFocusLost = true, + SelectAllOnFocus = true, + AllowDecimals = true, + OnInputError = () => + { + flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); + flashLayer.FadeOutFromOne(200, Easing.OutQuint); + } + }, + slider = new Slider + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + Width = 0.5f, + Current = Current, + } + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + focusManager = GetContainingFocusManager()!; + + textBox.Focused.BindValueChanged(_ => updateState()); + textBox.OnCommit += textCommitted; + textBox.Current.BindValueChanged(textChanged); + + current.BindValueChanged(_ => + { + updateState(); + updateTextBoxFromSlider(); + }, true); + } + + private bool updatingFromTextBox; + + private void textChanged(ValueChangedEvent change) + { + if (!instantaneous) return; + + tryUpdateSliderFromTextBox(); + } + + private void textCommitted(TextBox t, bool isNew) + { + tryUpdateSliderFromTextBox(); + + // If the attempted update above failed, restore text box to match the slider. + Current.TriggerChange(); + + flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2); + flashLayer.FadeOutFromOne(800, Easing.OutQuint); + } + + private void tryUpdateSliderFromTextBox() + { + updatingFromTextBox = true; + + try + { + switch (Current) + { + case Bindable bindableInt: + bindableInt.Value = int.Parse(textBox.Current.Value); + break; + + case Bindable bindableDouble: + bindableDouble.Value = double.Parse(textBox.Current.Value); + break; + + default: + Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture); + break; + } + } + catch + { + // ignore parsing failures. + // sane state will eventually be restored by a commit (either explicit, or implicit via focus loss). + } + + updatingFromTextBox = false; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + protected override bool OnClick(ClickEvent e) + { + focusManager.ChangeFocus(textBox); + return true; + } + + private void updateState() + { + textBox.Alpha = 1; + + background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5; + caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0; + BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; + + if (textBox.Focused.Value) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + background.Colour = colourProvider.Background5; + } + + private void updateTextBoxFromSlider() + { + if (updatingFromTextBox) return; + + textBox.Text = slider.GetDisplayableValue(Current.Value).ToString(); + } + + private partial class Slider : OsuSliderBar + { + private Box leftBox = null!; + private Box rightBox = null!; + private Circle nub = null!; + private const float nub_width = 10; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = 40; + RelativeSizeAxes = Axes.X; + RangePadding = nub_width / 2; + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + leftBox = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + rightBox = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = RangePadding, }, + Child = nub = new Circle + { + Width = nub_width, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Origin = Anchor.TopCentre, + } + }, + new HoverClickSounds() + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateState(); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + leftBox.Width = Math.Clamp(RangePadding + nub.DrawPosition.X, 0, Math.Max(0, DrawWidth)) / DrawWidth; + rightBox.Width = Math.Clamp(DrawWidth - nub.DrawPosition.X - RangePadding, 0, Math.Max(0, DrawWidth)) / DrawWidth; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } + + private void updateState() + { + rightBox.Colour = colourProvider.Background6; + leftBox.Colour = IsHovered ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2; + nub.Colour = IsHovered ? colourProvider.Highlight1 : colourProvider.Light4; + } + + protected override void UpdateValue(float value) + { + nub.MoveToX(value, 250, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 044576c635..741bff6db6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -122,7 +122,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 if (!current.Disabled && !ReadOnly) { - flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark1.Opacity(0), colourProvider.Dark2); + flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2); flashLayer.FadeOutFromOne(800, Easing.OutQuint); } }; From e0f92bab6a7d981642eca24c3ee2e8d73b19446c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 13:07:52 +0200 Subject: [PATCH 204/308] Add test case covering failure --- .../Editor/TestSceneManiaSelectionHandler.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs index b48f579ec0..4285ef2029 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaSelectionHandler.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Visual; @@ -92,5 +93,30 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250)); AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250)); } + + [Test] + public void TestOffScreenObjectsRemainSelectedOnColumnChange() + { + AddStep("create objects", () => + { + for (int i = 0; i < 20; ++i) + EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = 0 }); + }); + + AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + AddStep("start drag", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.PressButton(MouseButton.Left); + }); + AddStep("end drag", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Last()); + InputManager.ReleaseButton(MouseButton.Left); + }); + + AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3)); + AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects)); + } } } From 20b1d762699ac7cb0bebf705ed7708f4ebc20ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Sep 2024 13:07:57 +0200 Subject: [PATCH 205/308] Ensure selection is preserved when moving selection between columns Closes https://github.com/ppy/osu/issues/29793. I believe that the sequence of events that makes this happens is as follows: - User selects a range of objects. Some of those objects are off-screen, and thus would be presumed to be not alive - except the blueprint container forces them to remain alive, because they're part of the selection. - User moves the selection to another column, which is implemented by temporarily removing the objects from the playfield, changing their column, and re-adding them. This sort of pattern is supposed to kick off the `HitObjectUsageTransferred` flow in `HitObjectUsageEventBuffer` - and it does... for objects that are *currently visible on screen* and thus would be alive regardless of `SetKeepAlive()`. However, this does not hold for objects that are off-screen - nothing ensures they are kept alive again after re-adding, and thus they inadvertently become dead. - Thus, this doesn't kick off the `BlueprintContainer` flows associated with transferring objects to another column, and instead fires the removal flows, which ensure that the off-screen objects that were being moved are instead deselected. I tried a few other options but found no better resolution than this - calling `SetKeepAlive()` directly would require making it public, which seems like a bad idea. There's really no good way to generically handle this either, because it is the ruleset that decides that its way of implementing this operation will be a removal and re-add of objects, so... --- osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 9ae2112b30..7e0991a4d4 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -104,8 +104,10 @@ namespace osu.Game.Rulesets.Mania.Edit int minColumn = int.MaxValue; int maxColumn = int.MinValue; + var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType().ToArray(); + // find min/max in an initial pass before actually performing the movement. - foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType()) + foreach (var obj in selectedObjects) { if (obj.Column < minColumn) minColumn = obj.Column; @@ -121,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Edit ((ManiaHitObject)h).Column += columnDelta; maniaPlayfield.Add(h); }); + + // `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern, + // leading to selections being sometimes partially dropped if some of the objects being moved are off screen + // (check blame for detailed explanation). + // thus, ensure that selection is preserved manually. + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.AddRange(selectedObjects); } } } From 76c5e743d7ce834701c07fc15df3a19339a3db32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 13:49:27 +0900 Subject: [PATCH 206/308] Remove opacity from old style dropdown menus These aren't used in many places, but we've since moved away from opacity in UI elements like this, so let's just nuke it here for legibility. Addresses https://github.com/ppy/osu/discussions/29906. --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 71ae149cf6..dc42216c55 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -75,7 +75,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader(true)] private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio) { - BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + BackgroundColour = colourProvider?.Background5 ?? Color4.Black; HoverColour = colourProvider?.Light4 ?? colours.PinkDarker; SelectionColour = colourProvider?.Background3 ?? colours.PinkDarker.Opacity(0.5f); @@ -397,7 +397,7 @@ namespace osu.Game.Graphics.UserInterface { bool hovered = Enabled.Value && IsHovered; var hoveredColour = colourProvider?.Light4 ?? colours.PinkDarker; - var unhoveredColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + var unhoveredColour = colourProvider?.Background5 ?? Color4.Black; Colour = Color4.White; Alpha = Enabled.Value ? 1 : 0.3f; From fd6b3b6b36bd7d88e4aa7e16393f2cbf6f8343d8 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 17 Sep 2024 22:25:18 -0700 Subject: [PATCH 207/308] Fix searching by clicking title/artist in beatmap overlay not following original language setting --- osu.Game/Online/Chat/MessageFormatter.cs | 2 ++ osu.Game/OsuGame.cs | 15 +++++++++++---- .../BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 77454c4775..0f444ccde9 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -340,6 +340,8 @@ namespace osu.Game.Online.Chat Spectate, OpenUserProfile, SearchBeatmapSet, + SearchBeatmapTitle, + SearchBeatmapArtist, OpenWiki, Custom, OpenChangelog, diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0ef6a94679..ffb145d7de 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -445,10 +445,17 @@ namespace osu.Game break; case LinkAction.SearchBeatmapSet: - if (link.Argument is RomanisableString romanisable) - SearchBeatmapSet(romanisable.GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript)); - else - SearchBeatmapSet(argString); + SearchBeatmapSet(argString); + break; + + case LinkAction.SearchBeatmapTitle: + string title = ((RomanisableString)link.Argument).GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript); + SearchBeatmapSet($@"title=""""{title}"""""); + break; + + case LinkAction.SearchBeatmapArtist: + string artist = ((RomanisableString)link.Argument).GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript); + SearchBeatmapSet($@"artist=""""{artist}"""""); break; case LinkAction.FilterBeatmapSetGenre: diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index f9e0c6c380..6ea16a9997 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Clear(); artist.Clear(); - title.AddLink(titleText, LinkAction.SearchBeatmapSet, $@"title=""""{titleText}"""""); + title.AddLink(titleText, LinkAction.SearchBeatmapTitle, titleText); title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); @@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); } - artist.AddLink(artistText, LinkAction.SearchBeatmapSet, $@"artist=""""{artistText}"""""); + artist.AddLink(artistText, LinkAction.SearchBeatmapArtist, artistText); if (setInfo.NewValue.TrackId != null) { From 2d993645af3c2cd7f0ee8d7f2dcdb065a0dfb0c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 15:03:55 +0900 Subject: [PATCH 208/308] Add test coverage of judgements not being synced when resuming a replay --- .../Visual/Gameplay/TestSceneSpectator.cs | 11 +++++--- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 ++ .../Visual/Spectator/TestSpectatorClient.cs | 26 +++++++++++++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 0de2b6a980..d8817e563c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD.JudgementCounter; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Gameplay; using osu.Game.Tests.Visual.Multiplayer; @@ -167,14 +168,16 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestSpectatingDuringGameplay() { start(); - sendFrames(300); + sendFrames(300, initialResultCount: 100); loadSpectatingScreen(); waitForPlayerCurrent(); - sendFrames(300); + sendFrames(300, initialResultCount: 100); AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000)); + AddAssert("check judgement counts are correct", () => player.ChildrenOfType().Single().Counters.Sum(c => c.ResultCount.Value), + () => Is.GreaterThanOrEqualTo(100)); } [Test] @@ -405,9 +408,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void checkPaused(bool state) => AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); - private void sendFrames(int count = 10, double startTime = 0) + private void sendFrames(int count = 10, double startTime = 0, int initialResultCount = 0) { - AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime)); + AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime, initialResultCount)); } private void loadSpectatingScreen() diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 44ddb8c187..9752918dfb 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -181,6 +181,8 @@ namespace osu.Game.Rulesets.Scoring } } + public IReadOnlyDictionary Statistics => ScoreResultCounts; + private bool beatmapApplied; protected readonly Dictionary ScoreResultCounts = new Dictionary(); diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 5aef85fa13..c27e7f15ca 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -13,6 +13,8 @@ using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -99,13 +101,24 @@ namespace osu.Game.Tests.Visual.Spectator /// The user to send frames for. /// The total number of frames to send. /// The time to start gameplay frames from. - public void SendFramesFromUser(int userId, int count, double startTime = 0) + /// Add a number of misses to frame header data for testing purposes. + public void SendFramesFromUser(int userId, int count, double startTime = 0, int initialResultCount = 0) { var frames = new List(); int currentFrameIndex = userNextFrameDictionary[userId]; int lastFrameIndex = currentFrameIndex + count - 1; + var scoreProcessor = new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()); + + for (int i = 0; i < initialResultCount; i++) + { + scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) + { + Type = HitResult.Miss, + }); + } + for (; currentFrameIndex <= lastFrameIndex; currentFrameIndex++) { // This is done in the next frame so that currentFrameIndex is updated to the correct value. @@ -130,7 +143,16 @@ namespace osu.Game.Tests.Visual.Spectator Combo = currentFrameIndex, TotalScore = (long)(currentFrameIndex * 123478 * RNG.NextDouble(0.99, 1.01)), Accuracy = RNG.NextDouble(0.98, 1), - }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray()); + Statistics = scoreProcessor.Statistics.ToDictionary(), + }, scoreProcessor, frames.ToArray()); + + if (initialResultCount > 0) + { + foreach (var f in frames) + f.Header = bundle.Header; + } + + scoreProcessor.ResetFromReplayFrame(frames.Last()); ((ISpectatorClient)this).UserSentFrames(userId, bundle); frames.Clear(); From c46e9cbce3d098dafe1cf5331d533fc00cf2aa1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 14:35:18 +0900 Subject: [PATCH 209/308] Tidy up `JudgementCounter` classes --- .../HUD/JudgementCounter/JudgementCount.cs | 18 ++++++++++++++++++ .../JudgementCountController.cs | 9 --------- .../HUD/JudgementCounter/JudgementCounter.cs | 7 +++---- .../JudgementCounterDisplay.cs | 2 +- 4 files changed, 22 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs new file mode 100644 index 0000000000..ad70e519a2 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD.JudgementCounter +{ + public struct JudgementCount + { + public LocalisableString DisplayName { get; set; } + + public HitResult[] Types { get; set; } + + public BindableInt ResultCount { get; set; } + } +} diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 8134c97bac..5a53a9edd3 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -67,14 +67,5 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter else count.ResultCount.Value++; } - - public struct JudgementCount - { - public LocalisableString DisplayName { get; set; } - - public HitResult[] Types { get; set; } - - public BindableInt ResultCount { get; set; } - } } } diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs index 45ed8d749b..d69416f34a 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounter.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD.JudgementCounter @@ -19,16 +18,16 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter public BindableBool ShowName = new BindableBool(); public Bindable Direction = new Bindable(); - public readonly JudgementCountController.JudgementCount Result; + public readonly JudgementCount Result; - public JudgementCounter(JudgementCountController.JudgementCount result) => Result = result; + public JudgementCounter(JudgementCount result) => Result = result; public OsuSpriteText ResultName = null!; private FillFlowContainer flowContainer = null!; private JudgementRollingCounter counter = null!; [BackgroundDependencyLoader] - private void load(OsuColour colours, IBindable ruleset) + private void load(OsuColour colours) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs index 25e5464205..bc953435b7 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCounterDisplay.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter } } - private JudgementCounter createCounter(JudgementCountController.JudgementCount info) => + private JudgementCounter createCounter(JudgementCount info) => new JudgementCounter(info) { State = { Value = Visibility.Hidden }, From 8f49876fe7458924801338d04545fbf39a34755d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 14:35:44 +0900 Subject: [PATCH 210/308] Re-sync judgement counter display after replay frame reset --- .../JudgementCountController.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 5a53a9edd3..7e9f3cba08 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Localisation; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -53,8 +52,40 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter { base.LoadComplete(); + scoreProcessor.OnResetFromReplayFrame += updateAllCounts; scoreProcessor.NewJudgement += judgement => updateCount(judgement, false); scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true); + + updateAllCounts(); + } + + private void updateAllCounts() + { + // This flow is made to handle cases of watching from the middle of a replay / spectating session. + // + // Once we get an initial state, we can rely on `NewJudgement` and `JudgementReverted`, so + // as a preemptive optimisation, only do a full re-sync if we have all-zero counts. + bool hasCounts = false; + + foreach (var r in results) + { + if (r.Value.ResultCount.Value > 0) + { + hasCounts = true; + break; + } + } + + if (hasCounts) + return; + + foreach (var kvp in scoreProcessor.Statistics) + { + if (!results.TryGetValue(kvp.Key, out var count)) + continue; + + count.ResultCount.Value = kvp.Value; + } } private void updateCount(JudgementResult judgement, bool revert) From aae98e6906450bbd4518e4ec33af8049a0508df4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 16:11:47 +0900 Subject: [PATCH 211/308] Add failing test showing crash at song select on selection edge case --- .../Navigation/TestSceneScreenNavigation.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index f02c2fd4f0..6cd89dcd0c 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -144,6 +144,22 @@ namespace osu.Game.Tests.Visual.Navigation exitViaEscapeAndConfirm(); } + [Test] + public void TestEnterGameplayWhileFilteringToNoSelection() + { + TestPlaySongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("force selection", () => + { + songSelect.FinaliseSelection(); + songSelect.FilterControl.CurrentTextSearch.Value = "test"; + }); + } + [Test] public void TestSongSelectBackActionHandling() { From c192a6a1d54b69936c6e741e49c7b0524c0a2c8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 15:50:55 +0900 Subject: [PATCH 212/308] Fix song select crashes due to attempting to clear selection after load has already begun --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 525884c413..57978b7bbd 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1036,7 +1036,7 @@ namespace osu.Game.Screens.Select itemsCache.Validate(); // update and let external consumers know about selection loss. - if (BeatmapSetsLoaded) + if (BeatmapSetsLoaded && AllowSelection) { bool selectionLost = selectedBeatmapSet != null && selectedBeatmapSet.State.Value != CarouselItemState.Selected; From 743d50924105a557a0d08424e7c274ee1996d580 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 16:40:27 +0900 Subject: [PATCH 213/308] Also ensure filter is applied when returning to song select --- .../Navigation/TestSceneScreenNavigation.cs | 6 +++++ osu.Game/Screens/Select/SongSelect.cs | 23 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 6cd89dcd0c..eda7ce925a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -150,6 +150,7 @@ namespace osu.Game.Tests.Visual.Navigation TestPlaySongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); @@ -158,6 +159,11 @@ namespace osu.Game.Tests.Visual.Navigation songSelect.FinaliseSelection(); songSelect.FilterControl.CurrentTextSearch.Value = "test"; }); + + AddUntilStep("wait for player", () => !songSelect.IsCurrentScreen()); + AddStep("return to song select", () => songSelect.MakeCurrent()); + + AddUntilStep("wait for selection lost", () => songSelect.Beatmap.IsDefault); } [Test] diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6da72ee660..18608d61e9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -127,6 +127,8 @@ namespace osu.Game.Screens.Select private Sample sampleChangeDifficulty = null!; private Sample sampleChangeBeatmap = null!; + private bool pendingFilterApplication; + private Container carouselContainer = null!; protected BeatmapDetailArea BeatmapDetails { get; private set; } = null!; @@ -328,7 +330,20 @@ namespace osu.Game.Screens.Select GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), }, c => carouselContainer.Child = c); - FilterControl.FilterChanged = Carousel.Filter; + FilterControl.FilterChanged = criteria => + { + // If a filter operation is applied when we're in a state that doesn't allow selection, + // we might end up in an unexpected state. This is because currently carousel panels are in charge + // of updating the global selection (which is very hard to deal with). + // + // For now let's just avoid filtering when selection isn't allowed locally. + // This should be nuked from existence when we get around to fixing the complexity of song select <-> beatmap carousel. + // The debounce part of BeatmapCarousel's filtering should probably also be removed and handled locally. + if (Carousel.AllowSelection) + Carousel.Filter(criteria); + else + pendingFilterApplication = true; + }; if (ShowSongSelectFooter) { @@ -701,6 +716,12 @@ namespace osu.Game.Screens.Select Carousel.AllowSelection = true; + if (pendingFilterApplication) + { + Carousel.Filter(FilterControl.CreateCriteria()); + pendingFilterApplication = false; + } + BeatmapDetails.Refresh(); beginLooping(); From ac507a3ba568e40396a642d13032dbc1e8d6c314 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Sep 2024 17:21:43 +0900 Subject: [PATCH 214/308] Remove unused parameter in `applyActiveCriteria` --- osu.Game/Screens/Select/BeatmapCarousel.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 57978b7bbd..d9359cfec3 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -706,7 +706,7 @@ namespace osu.Game.Screens.Select private bool beatmapsSplitOut; - private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) + private void applyActiveCriteria(bool debounce) { PendingFilter?.Cancel(); PendingFilter = null; @@ -735,8 +735,7 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - if (alwaysResetScrollPosition || !Scroll.UserScrolling) - ScrollToSelected(true); + ScrollToSelected(true); FilterApplied?.Invoke(); } From 95e26e6fd8a548d1d0443e52e838ca68d9bc7319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 11:23:00 +0200 Subject: [PATCH 215/308] Make slider bar instantaneous by default (and fix broken implementation) --- .../UserInterface/TestSceneFormControls.cs | 16 +++++++++++++--- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 8 ++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 6dd7275abf..369fe1a40c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Cursor; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; -using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Tests.Visual.UserInterface @@ -72,8 +71,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, new FormSliderBar { - Caption = BeatmapsetsStrings.ShowStatsDrain, - HintText = EditorSetupStrings.DrainRateDescription, + Caption = "Instantaneous slider", Current = new BindableFloat { MinValue = 0, @@ -82,6 +80,18 @@ namespace osu.Game.Tests.Visual.UserInterface Precision = 0.1f, } }, + new FormSliderBar + { + Caption = "Non-instantaneous slider", + Current = new BindableFloat + { + MinValue = 0, + MaxValue = 10, + Value = 5, + Precision = 0.1f, + }, + Instantaneous = false, + }, }, }, } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 91ce9da2d2..e4c814e71d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -7,6 +7,7 @@ using System.Numerics; 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.Colour; using osu.Framework.Graphics.Containers; @@ -29,7 +30,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => current.Current = value; } - private bool instantaneous; + private bool instantaneous = true; /// /// Whether changes to the slider should instantaneously transfer to the text box (and vice versa). @@ -41,7 +42,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 set { instantaneous = value; - slider.TransferValueOnCommit = !instantaneous; + + if (slider.IsNotNull()) + slider.TransferValueOnCommit = !instantaneous; } } @@ -116,6 +119,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 RelativeSizeAxes = Axes.X, Width = 0.5f, Current = Current, + TransferValueOnCommit = !instantaneous, } }, }, From 0bab755be316d46ef82700f7cd9e0f916202db46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 11:25:23 +0200 Subject: [PATCH 216/308] Add missing xmldoc --- osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs | 7 +++++++ osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs | 7 +++++++ osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs index 797ff09800..d4cd86010f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormCheckBox.cs @@ -29,7 +29,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 private readonly BindableWithCurrent current = new BindableWithCurrent(); + /// + /// Caption describing this slider bar, displayed on top of the controls. + /// public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. + /// public LocalisableString HintText { get; init; } private Box background = null!; diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index e4c814e71d..1d44c5d810 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -50,7 +50,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); + /// + /// Caption describing this slider bar, displayed on top of the controls. + /// public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. + /// public LocalisableString HintText { get; init; } private Box background = null!; diff --git a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs index 741bff6db6..9bbb5cba99 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormTextBox.cs @@ -59,8 +59,19 @@ namespace osu.Game.Graphics.UserInterfaceV2 private readonly BindableWithCurrent current = new BindableWithCurrent(); + /// + /// Caption describing this slider bar, displayed on top of the controls. + /// public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. + /// public LocalisableString HintText { get; init; } + + /// + /// Text displayed in the text box when its contents are empty. + /// public LocalisableString PlaceholderText { get; init; } private Box background = null!; From 093d9ab076129cf732e21849f5ee49a0185a451d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 11:30:52 +0200 Subject: [PATCH 217/308] Keep slider bar looking active when dragging outside of its bounds --- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 1d44c5d810..fa6d44d4c5 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -143,6 +143,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 textBox.OnCommit += textCommitted; textBox.Current.BindValueChanged(textChanged); + slider.IsDragging.BindValueChanged(_ => updateState()); + current.BindValueChanged(_ => { updateState(); @@ -226,12 +228,12 @@ namespace osu.Game.Graphics.UserInterfaceV2 caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; - BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0; + BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0; BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; if (textBox.Focused.Value) background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); - else if (IsHovered) + else if (IsHovered || slider.IsDragging.Value) background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); else background.Colour = colourProvider.Background5; @@ -246,6 +248,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 private partial class Slider : OsuSliderBar { + public BindableBool IsDragging { get; set; } = new BindableBool(); + private Box leftBox = null!; private Box rightBox = null!; private Circle nub = null!; @@ -313,6 +317,21 @@ namespace osu.Game.Graphics.UserInterfaceV2 rightBox.Width = Math.Clamp(DrawWidth - nub.DrawPosition.X - RangePadding, 0, Math.Max(0, DrawWidth)) / DrawWidth; } + protected override bool OnDragStart(DragStartEvent e) + { + bool dragging = base.OnDragStart(e); + IsDragging.Value = dragging; + updateState(); + return dragging; + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + IsDragging.Value = false; + updateState(); + } + protected override bool OnHover(HoverEvent e) { updateState(); @@ -328,8 +347,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void updateState() { rightBox.Colour = colourProvider.Background6; - leftBox.Colour = IsHovered ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2; - nub.Colour = IsHovered ? colourProvider.Highlight1 : colourProvider.Light4; + leftBox.Colour = IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2; + nub.Colour = IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4; } protected override void UpdateValue(float value) From d506d8a1500f73a3b93473f155541073957b7a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 11:32:55 +0200 Subject: [PATCH 218/308] Implement `TabbableContentContainer` for slider control --- .../UserInterface/TestSceneFormControls.cs | 4 +++- .../Graphics/UserInterfaceV2/FormSliderBar.cs | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 369fe1a40c..b456da0f26 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -78,7 +78,8 @@ namespace osu.Game.Tests.Visual.UserInterface MaxValue = 10, Value = 5, Precision = 0.1f, - } + }, + TabbableContentContainer = this, }, new FormSliderBar { @@ -91,6 +92,7 @@ namespace osu.Game.Tests.Visual.UserInterface Precision = 0.1f, }, Instantaneous = false, + TabbableContentContainer = this, }, }, }, diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index fa6d44d4c5..84becb72c9 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -48,6 +48,19 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } + private CompositeDrawable? tabbableContentContainer; + + public CompositeDrawable? TabbableContentContainer + { + set + { + tabbableContentContainer = value; + + if (textBox.IsNotNull()) + textBox.TabbableContentContainer = tabbableContentContainer; + } + } + private readonly BindableNumberWithCurrent current = new BindableNumberWithCurrent(); /// @@ -117,7 +130,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); flashLayer.FadeOutFromOne(200, Easing.OutQuint); - } + }, + TabbableContentContainer = tabbableContentContainer, }, slider = new Slider { From 2d3b027f85d7c30fe269ac8fa99bfca0dcd1555e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 15:18:11 +0200 Subject: [PATCH 219/308] Add test case covering desired behaviour --- .../Editing/TestSceneComposerSelection.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 3884a3108f..765d7ee21e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -215,6 +215,54 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); } + [Test] + public void TestMultiSelectWithDragBox() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(100) }, + new HitCircle { StartTime = 300, Position = new Vector2(512, 0) }, + new HitCircle { StartTime = 400, Position = new Vector2(412, 100) }, + }; + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("start dragging", () => + { + InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + }); + AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopLeft - new Vector2(5))); + AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2)); + + AddStep("start dragging with control", () => + { + InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + InputManager.PressKey(Key.ControlLeft); + }); + AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5))); + AddStep("end dragging", () => + { + InputManager.ReleaseButton(MouseButton.Left); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4)); + + AddStep("start dragging without control", () => + { + InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + }); + AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5))); + AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2)); + } + [Test] public void TestNearestSelection() { From f6195c551547e3b801563b86f9df17e4f4b81182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 18 Sep 2024 15:02:04 +0200 Subject: [PATCH 220/308] Add to existing selection when dragging with control pressed Closes https://github.com/ppy/osu/issues/29023. --- .../Edit/Compose/Components/BlueprintContainer.cs | 14 +++++++++++--- .../Timeline/TimelineBlueprintContainer.cs | 5 ++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c66be90605..9776e64855 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -196,6 +196,11 @@ namespace osu.Game.Screens.Edit.Compose.Components DragBox.HandleDrag(e); DragBox.Show(); + + selectionBeforeDrag.Clear(); + if (e.ControlPressed) + selectionBeforeDrag.UnionWith(SelectedItems); + return true; } @@ -217,6 +222,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } DragBox.Hide(); + selectionBeforeDrag.Clear(); } protected override void Update() @@ -227,7 +233,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { lastDragEvent.Target = this; DragBox.HandleDrag(lastDragEvent); - UpdateSelectionFromDragBox(); + UpdateSelectionFromDragBox(selectionBeforeDrag); } } @@ -472,7 +478,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Select all blueprints in a selection area specified by . /// - protected virtual void UpdateSelectionFromDragBox() + protected virtual void UpdateSelectionFromDragBox(HashSet selectionBeforeDrag) { var quad = DragBox.Box.ScreenSpaceDrawQuad; @@ -482,7 +488,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { case SelectionState.Selected: // Selection is preserved even after blueprint becomes dead. - if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint)) + if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint) && !selectionBeforeDrag.Contains(blueprint.Item)) blueprint.Deselect(); break; @@ -535,6 +541,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private bool wasDragStarted; + private readonly HashSet selectionBeforeDrag = new HashSet(); + /// /// Attempts to begin the movement of any selected blueprints. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 740f0b6aac..a6af83d268 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected sealed override DragBox CreateDragBox() => new TimelineDragBox(); - protected override void UpdateSelectionFromDragBox() + protected override void UpdateSelectionFromDragBox(HashSet selectionBeforeDrag) { Composer.BlueprintContainer.CommitIfPlacementActive(); @@ -191,6 +191,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline bool shouldBeSelected(HitObject hitObject) { + if (selectionBeforeDrag.Contains(hitObject)) + return true; + double midTime = (hitObject.StartTime + hitObject.GetEndTime()) / 2; return minTime <= midTime && midTime <= maxTime; } From c185acdbae367902f84e803b12d62ebd95c0566d Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 18 Sep 2024 11:16:25 -0700 Subject: [PATCH 221/308] Use `GetLocalisedBindableString()` instead --- osu.Game/Online/Chat/MessageFormatter.cs | 2 -- osu.Game/OsuGame.cs | 18 ++++++++---------- .../BeatmapSet/BeatmapSetHeaderContent.cs | 4 ++-- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 0f444ccde9..77454c4775 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -340,8 +340,6 @@ namespace osu.Game.Online.Chat Spectate, OpenUserProfile, SearchBeatmapSet, - SearchBeatmapTitle, - SearchBeatmapArtist, OpenWiki, Custom, OpenChangelog, diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index ffb145d7de..1af86b2d83 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -445,17 +445,15 @@ namespace osu.Game break; case LinkAction.SearchBeatmapSet: - SearchBeatmapSet(argString); - break; + if (link.Argument is LocalisableString localisable) + { + var localised = Localisation.GetLocalisedBindableString(localisable); + SearchBeatmapSet(localised.Value); + localised.UnbindAll(); + } + else + SearchBeatmapSet(argString); - case LinkAction.SearchBeatmapTitle: - string title = ((RomanisableString)link.Argument).GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript); - SearchBeatmapSet($@"title=""""{title}"""""); - break; - - case LinkAction.SearchBeatmapArtist: - string artist = ((RomanisableString)link.Argument).GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript); - SearchBeatmapSet($@"artist=""""{artist}"""""); break; case LinkAction.FilterBeatmapSetGenre: diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 6ea16a9997..a50043f0f0 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Clear(); artist.Clear(); - title.AddLink(titleText, LinkAction.SearchBeatmapTitle, titleText); + title.AddLink(titleText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"title=""""{titleText}""""")); title.AddArbitraryDrawable(Empty().With(d => d.Width = 5)); title.AddArbitraryDrawable(externalLink = new ExternalLinkButton()); @@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet title.AddArbitraryDrawable(new SpotlightBeatmapBadge()); } - artist.AddLink(artistText, LinkAction.SearchBeatmapArtist, artistText); + artist.AddLink(artistText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"artist=""""{artistText}""""")); if (setInfo.NewValue.TrackId != null) { From d0519238a3a74f507da725035ce9cba5ad757cb0 Mon Sep 17 00:00:00 2001 From: Neku Date: Wed, 18 Sep 2024 22:57:37 +0200 Subject: [PATCH 222/308] Implement beat-synchronized animation in skip overlay --- osu.Game/Screens/Play/SkipOverlay.cs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 29b2e5229b..c88724c8db 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -16,6 +17,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -26,7 +28,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play { - public partial class SkipOverlay : CompositeDrawable, IKeyBindingHandler + public partial class SkipOverlay : BeatSyncedContainer, IKeyBindingHandler { /// /// The total number of successful skips performed by this overlay. @@ -36,10 +38,9 @@ namespace osu.Game.Screens.Play private readonly double startTime; public Action RequestSkip; - private Button button; private ButtonContainer buttonContainer; - private Box remainingTimeBox; + private Circle remainingTimeBox; private FadeContainer fadeContainer; private double displayTime; @@ -51,7 +52,6 @@ namespace osu.Game.Screens.Play private IGameplayClock gameplayClock { get; set; } internal bool IsButtonVisible => fadeContainer.State == Visibility.Visible && buttonContainer.State.Value == Visibility.Visible; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; /// @@ -87,13 +87,13 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - remainingTimeBox = new Box + remainingTimeBox = new Circle { Height = 5, - RelativeSizeAxes = Axes.X, - Colour = colours.Yellow, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, + Colour = colours.Yellow, + RelativeSizeAxes = Axes.X } } } @@ -210,6 +210,18 @@ namespace osu.Game.Screens.Play { } + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (fadeOutBeginTime <= gameplayClock.CurrentTime) + return; + + float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime); + float newWidth = Math.Max(0, 1 - Math.Clamp(progress, 0, 1)); + remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 2, Easing.OutQuint); + } + public partial class FadeContainer : Container, IStateful { [CanBeNull] From fdd94aa8452ac5d70bd139d22f700d6e456947f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Sep 2024 09:41:54 +0200 Subject: [PATCH 223/308] Remove pointless max The clamp should already ensure this. --- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index c88724c8db..362677ca5c 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -218,7 +218,7 @@ namespace osu.Game.Screens.Play return; float progress = (float)(gameplayClock.CurrentTime - displayTime) / (float)(fadeOutBeginTime - displayTime); - float newWidth = Math.Max(0, 1 - Math.Clamp(progress, 0, 1)); + float newWidth = 1 - Math.Clamp(progress, 0, 1); remainingTimeBox.ResizeWidthTo(newWidth, timingPoint.BeatLength * 2, Easing.OutQuint); } From fd45644d0f58f6eb90916aa820628b26918b60e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Sep 2024 16:54:00 +0900 Subject: [PATCH 224/308] Fix skin layout editor `PlayerAvatar` applying corner radius weirdly after scale Closes #29919. I've also made this handle resizing better, so now you can have non-square avatar displays. --- osu.Game/Screens/Play/HUD/PlayerAvatar.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs index 06d0f7bc9a..8e4406c2c1 100644 --- a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs +++ b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs @@ -39,14 +39,23 @@ namespace osu.Game.Screens.Play.HUD private IBindable? apiUser; + private readonly Container cornerContainer; + public PlayerAvatar() { Size = new Vector2(default_size); - InternalChild = avatar = new UpdateableAvatar(isInteractive: false) + InternalChild = cornerContainer = new Container { + Masking = true, RelativeSizeAxes = Axes.Both, - Masking = true + Child = avatar = new UpdateableAvatar(isInteractive: false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + } }; } @@ -66,7 +75,7 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - CornerRadius.BindValueChanged(e => avatar.CornerRadius = e.NewValue * default_size, true); + CornerRadius.BindValueChanged(e => cornerContainer.CornerRadius = e.NewValue * default_size, true); } public bool UsesFixedAnchor { get; set; } From ca8402d98021b6a83d5df5ec119c661bca38152c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Sep 2024 19:06:46 +0900 Subject: [PATCH 225/308] Make animation slightly more snappy --- osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs index 84becb72c9..ac3730598f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormSliderBar.cs @@ -367,7 +367,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override void UpdateValue(float value) { - nub.MoveToX(value, 250, Easing.OutQuint); + nub.MoveToX(value, 200, Easing.OutPow10); } } } From d5c2484109ccf55ae7169061696797d1b17e12bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Sep 2024 19:23:14 +0900 Subject: [PATCH 226/308] Always transfer updated counts once --- .../JudgementCountController.cs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 7e9f3cba08..2562e26127 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -55,28 +55,13 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter scoreProcessor.OnResetFromReplayFrame += updateAllCounts; scoreProcessor.NewJudgement += judgement => updateCount(judgement, false); scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true); - - updateAllCounts(); } + private bool hasUpdatedCounts; + private void updateAllCounts() { - // This flow is made to handle cases of watching from the middle of a replay / spectating session. - // - // Once we get an initial state, we can rely on `NewJudgement` and `JudgementReverted`, so - // as a preemptive optimisation, only do a full re-sync if we have all-zero counts. - bool hasCounts = false; - - foreach (var r in results) - { - if (r.Value.ResultCount.Value > 0) - { - hasCounts = true; - break; - } - } - - if (hasCounts) + if (hasUpdatedCounts) return; foreach (var kvp in scoreProcessor.Statistics) @@ -86,6 +71,8 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter count.ResultCount.Value = kvp.Value; } + + hasUpdatedCounts = true; } private void updateCount(JudgementResult judgement, bool revert) From 89509ea49ed608c2c587816c35833f1822490eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Sep 2024 14:03:49 +0200 Subject: [PATCH 227/308] Fix `DrawableOsuHitObject` not properly cleaning up dim application callbacks Should fix https://github.com/ppy/osu/issues/28629. First of all, to support the claim that this does fix the issue - reproduction is rather difficult, but I believe I found a way to maximise the chances of it reproducing by performing the following steps: 1. Apply the following diff: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index eacd2b3e75..4c00da031a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -95,6 +96,8 @@ public DrawableSlider([CanBeNull] Slider s = null) [BackgroundDependencyLoader] private void load() { + Thread.Sleep(100); + tailContainer = new Container { RelativeSizeAxes = Axes.Both }; AddRangeInternal(new Drawable[] 2. Download https://osu.ppy.sh/beatmapsets/1470790#osu/3023028 and open it in the editor. 3. Select all objects using Ctrl-A. Yes, it'll take a while, especially so with the patch above. 4. Rotate the selection by any amount using the right toolbox. 5. Press undo. The game should crash. If it doesn't spam redo and undo until it does. Now to explain what the fix is. In the issue thread I spent a considerable time hemming and hawing about which of the dimmable pieces was null, which was a complete miss and a failure at reading. Let's see the stack trace again: 2024-06-27 02:15:20 [error]: at osu.Game.Rulesets.Osu.Objects.Drawables.DrawableOsuHitObject.g__applyDim|15_0(Drawable piece) in /home/runner/work/osu-auth-client/osu-auth-client/osu/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs:line 101 Line 101, you say? What could be null here? https://github.com/ppy/osu/blob/bd8addfb5f71568479d2c037d1b6e811de6e7fe6/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs#L101 Okay, what's `InitialLifetimeOffset`, then? https://github.com/ppy/osu/blob/bd8addfb5f71568479d2c037d1b6e811de6e7fe6/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs#L108 Yes, that's right. It's `HitObject` that is null here. Now why does *that* happen? First, let's note that all stacks where this died went through `UpdateState()`, which means that the problematic `applyDim()` calls had to be `ApplyCustomUpdateState` event callbacks. Meaning that the pieces where `HitObject` was null were DHOs themselves. Recall that parent DHOs and child DHOs are pooled separately. Therefore, there is no guarantee that any parent and child DHOs will remain associated with each other for the entire duration of a gameplay session; it is quite the contrary, and nobody should rely on that. Unfortunately for us, adding a `applyDimToDrawableHitObject` callback to a child object's `ApplyCustomUpdateState` *implicitly creates* such an association, because it ends up allocating a closure that captures `this` (meaning the parent in this context). Therefore, this now creates a situation where a child DHO can attempt to read state from a former parent DHO which can be in an indeterminate state, and in fact, when this crashes, the former parent DHO is most likely not even in use - hence the null `HitObject`. Thus, the fix is to clear the association by unsubscribing from the event when nested objects are cleared. My hypothesis why the reproduction scenario is like it is, is that both the sleep and the increased pressure on the pool (by way of selecting all objects and therefore forcing the DHOs to be materialised beyond pool capacity) increases the likelihood of getting a crosslink. When pool pressure is low, it is much more likely that a parent DHO *will* get the same child DHO again on re-application, even though that is not guaranteed. Just as an additional detail, note that the sentry issue for this lists the "first seen" version as 2024.312.0, which is the release that included https://github.com/ppy/osu/pull/27401 which would be directly responsible for this mess. --- .../Objects/Drawables/DrawableOsuHitObject.cs | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 5f5deca1ba..b3a68ec92d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -91,20 +91,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables drawableObjectPiece.ApplyCustomUpdateState -= applyDimToDrawableHitObject; drawableObjectPiece.ApplyCustomUpdateState += applyDimToDrawableHitObject; } - else - applyDim(piece); - } - void applyDim(Drawable piece) - { - piece.FadeColour(new Color4(195, 195, 195, 255)); - using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW)) - piece.FadeColour(Color4.White, 100); + // but at the end apply the transforms now regardless of whether this is a DHO or not. + // the above is just to ensure they don't get overwritten later. + applyDim(piece); } - - void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho); } + protected override void ClearNestedHitObjects() + { + base.ClearNestedHitObjects(); + + // any dimmable pieces that are DHOs will be pooled separately. + // `applyDimToDrawableHitObject` is a closure that implicitly captures `this`, + // and because of separate pooling of parent and child objects, there is no guarantee that the pieces will be associated with `this` again on re-use. + // therefore, clean up the subscription here to avoid crosstalk. + // not doing so can result in the callback attempting to read things from `this` when it is in a completely bogus state (not in use or similar). + foreach (var piece in DimmablePieces.OfType()) + piece.ApplyCustomUpdateState -= applyDimToDrawableHitObject; + } + + private void applyDim(Drawable piece) + { + piece.FadeColour(new Color4(195, 195, 195, 255)); + using (piece.BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW)) + piece.FadeColour(Color4.White, 100); + } + + private void applyDimToDrawableHitObject(DrawableHitObject dho, ArmedState _) => applyDim(dho); + protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; private OsuInputManager osuActionInputManager; From 8c72feda09feeac72b91542081cf745eaf5fbd97 Mon Sep 17 00:00:00 2001 From: PowerDaniex <140076282+u4vh3@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:36:01 +0200 Subject: [PATCH 228/308] Add colour customization to the layout editor --- .../Configuration/SettingSourceAttribute.cs | 13 +++ osu.Game/Overlays/Settings/SettingsColour.cs | 79 +++++++++++++++++++ osu.Game/Overlays/SkinEditor/SkinEditor.cs | 5 ++ 3 files changed, 97 insertions(+) create mode 100644 osu.Game/Overlays/Settings/SettingsColour.cs diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1e425c88a6..3ba46144ca 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -186,6 +186,16 @@ namespace osu.Game.Configuration break; + case BindableColour4 bColour: + yield return new SettingsColour + { + LabelText = attr.Label, + TooltipText = attr.Description, + Current = bColour + }; + + break; + case IBindable bindable: var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]); var dropdown = (Drawable)Activator.CreateInstance(dropdownType)!; @@ -227,6 +237,9 @@ namespace osu.Game.Configuration case Bindable b: return b.Value; + case BindableColour4 c: + return c.Value.ToHex(); + case IBindable u: // An unknown (e.g. enum) generic type. var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); diff --git a/osu.Game/Overlays/Settings/SettingsColour.cs b/osu.Game/Overlays/Settings/SettingsColour.cs new file mode 100644 index 0000000000..a58c20adea --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsColour.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Overlays.Settings +{ + public partial class SettingsColour : SettingsItem + { + protected override Drawable CreateControl() => new ColourControl(); + + public partial class ColourControl : OsuClickableContainer, IHasPopover, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly Box fill; + private readonly OsuSpriteText colourHexCode; + + public ColourControl() + { + RelativeSizeAxes = Axes.X; + Height = 40; + CornerRadius = 20; + Masking = true; + Action = this.ShowPopover; + + Children = new Drawable[] + { + fill = new Box + { + RelativeSizeAxes = Axes.Both + }, + colourHexCode = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Default.With(size: 20) + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateColour(), true); + } + + private void updateColour() + { + fill.Colour = Current.Value; + colourHexCode.Text = Current.Value.ToHex(); + colourHexCode.Colour = OsuColour.ForegroundTextColourFor(Current.Value); + } + + public Popover GetPopover() => new OsuPopover(false) + { + Child = new OsuColourPicker + { + Current = { BindTarget = Current } + } + }; + } + } +} diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index d1e9676de7..b8e859bd63 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -33,6 +33,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Skinning; +using osu.Framework.Graphics.Cursor; namespace osu.Game.Overlays.SkinEditor { @@ -117,6 +118,9 @@ namespace osu.Game.Overlays.SkinEditor InternalChild = new OsuContextMenuContainer { + RelativeSizeAxes = Axes.Both, + Child = new PopoverContainer + { RelativeSizeAxes = Axes.Both, Child = new GridContainer { @@ -221,6 +225,7 @@ namespace osu.Game.Overlays.SkinEditor }, } } + } }; clipboardContent = clipboard.Content.GetBoundCopy(); From 6ec3f715d2a796413ab4e29b11d76413356725d8 Mon Sep 17 00:00:00 2001 From: PowerDaniex <140076282+u4vh3@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:39:10 +0200 Subject: [PATCH 229/308] Fix formatting --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 182 ++++++++++----------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index b8e859bd63..6f7781ee9c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -120,112 +120,112 @@ namespace osu.Game.Overlays.SkinEditor { RelativeSizeAxes = Axes.Both, Child = new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - Child = new GridContainer { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Child = new GridContainer { - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - }, + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, - Content = new[] - { - new Drawable[] + Content = new[] { - new Container + new Drawable[] { - Name = @"Menu container", - RelativeSizeAxes = Axes.X, - Depth = float.MinValue, - Height = MENU_HEIGHT, - Children = new Drawable[] + new Container { - new EditorMenuBar + Name = @"Menu container", + RelativeSizeAxes = Axes.X, + Depth = float.MinValue, + Height = MENU_HEIGHT, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Items = new[] + new EditorMenuBar { - new MenuItem(CommonStrings.MenuBarFile) - { - Items = new OsuMenuItem[] - { - new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), - new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), - new OsuMenuItemSpacer(), - new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), - }, - }, - new MenuItem(CommonStrings.MenuBarEdit) - { - Items = new OsuMenuItem[] - { - undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new OsuMenuItemSpacer(), - cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), - copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), - pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), - cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), - } - }, - } - }, - headerText = new OsuTextFlowContainer - { - TextAnchor = Anchor.TopRight, - Padding = new MarginPadding(5), - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - }, - }, - }, - }, - new Drawable[] - { - new SkinEditorSceneLibrary - { - RelativeSizeAxes = Axes.X, - }, - }, - new Drawable[] - { - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - componentsSidebar = new EditorSidebar(), - content = new Container - { - Depth = float.MaxValue, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, + Items = new[] + { + new MenuItem(CommonStrings.MenuBarFile) + { + Items = new OsuMenuItem[] + { + new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new OsuMenuItemSpacer(), + new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), + new OsuMenuItemSpacer(), + new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), + }, + }, + new MenuItem(CommonStrings.MenuBarEdit) + { + Items = new OsuMenuItem[] + { + undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), + redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), + new OsuMenuItemSpacer(), + cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste), + cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone), + } + }, + } }, - settingsSidebar = new EditorSidebar(), + headerText = new OsuTextFlowContainer + { + TextAnchor = Anchor.TopRight, + Padding = new MarginPadding(5), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + }, + }, + }, + }, + new Drawable[] + { + new SkinEditorSceneLibrary + { + RelativeSizeAxes = Axes.X, + }, + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + componentsSidebar = new EditorSidebar(), + content = new Container + { + Depth = float.MaxValue, + RelativeSizeAxes = Axes.Both, + }, + settingsSidebar = new EditorSidebar(), + } } } - } - }, + }, + } } } - } }; clipboardContent = clipboard.Content.GetBoundCopy(); From e81e356d59afc04d13e6d1738ef1c1fe8445879e Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Tue, 6 Aug 2024 11:06:08 +0200 Subject: [PATCH 230/308] Add colour customisation to skin components --- .../SkinnableComponentStrings.cs | 20 +++++++++++++++++++ .../Screens/Play/HUD/ArgonSongProgress.cs | 4 ++++ osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs | 7 ++++++- .../Screens/Play/HUD/DefaultSongProgress.cs | 5 +++++ .../Components/BeatmapAttributeText.cs | 2 ++ osu.Game/Skinning/Components/BoxElement.cs | 4 ++++ osu.Game/Skinning/Components/PlayerName.cs | 2 ++ osu.Game/Skinning/Components/TextElement.cs | 2 ++ .../Skinning/FontAdjustableSkinComponent.cs | 8 ++++++++ 9 files changed, 53 insertions(+), 1 deletion(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index d5c8d5ccec..bd22527f67 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -59,6 +59,26 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown."); + /// + /// "Colour" + /// + public static LocalisableString Colour => new TranslatableString(getKey(@"colour"), @"Colour"); + + /// + /// "The colour of the component." + /// + public static LocalisableString ColourDescription => new TranslatableString(getKey(@"colour_description"), @"The colour of the component."); + + /// + /// "Font colour" + /// + public static LocalisableString FontColour => new TranslatableString(getKey(@"font_colour"), @"Font colour"); + + /// + /// "The colour of the font." + /// + public static LocalisableString FontColourDescription => new TranslatableString(getKey(@"font_colour_description"), @"The colour of the font."); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index ebebfebfb3..696369921a 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Localisation.HUD; +using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Play.HUD @@ -28,6 +29,8 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); [Resolved] private Player? player { get; set; } @@ -114,6 +117,7 @@ namespace osu.Game.Screens.Play.HUD base.Update(); content.Height = bar.Height + bar_height + info.Height; graphContainer.Height = bar.Height; + base.Colour = Colour.Value; } protected override void UpdateProgress(double progress, bool isIntro) diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs index 3c2e3e05ea..837e9547f0 100644 --- a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; using osu.Game.Skinning; using osuTK; @@ -21,6 +22,9 @@ namespace osu.Game.Screens.Play.HUD [SettingSource("Inverted shear")] public BindableBool InvertShear { get; } = new BindableBool(); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + public new BindableColour4 Colour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF")); + public ArgonWedgePiece() { CornerRadius = 10f; @@ -37,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD InternalChild = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66CCFF").Opacity(0.0f), Color4Extensions.FromHex("#66CCFF").Opacity(0.25f)), + Colour = ColourInfo.GradientVertical(Colour.Value.Opacity(0.0f), Colour.Value.Opacity(0.25f)), }; } @@ -46,6 +50,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); + Colour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(Colour.Value.Opacity(0.0f), Colour.Value.Opacity(0.25f))); } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 6b2bb2b718..512edd7106 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -10,6 +10,7 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Localisation.HUD; +using osu.Game.Localisation.SkinComponents; using osu.Game.Rulesets.Objects; using osuTK; @@ -35,6 +36,8 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); [Resolved] private Player? player { get; set; } @@ -114,6 +117,8 @@ namespace osu.Game.Screens.Play.HUD if (!Precision.AlmostEquals(Height, newHeight, 5f)) content.Height = newHeight; + + base.Colour = Colour.Value; } private void updateBarVisibility() diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index c467b2e946..06f0d9cea9 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -123,6 +123,8 @@ namespace osu.Game.Skinning.Components } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + + protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; } // WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END. diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index 34d389728c..e49ec0cc4d 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -27,6 +27,9 @@ namespace osu.Game.Skinning.Components Precision = 0.01f }; + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] + public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); + public BoxElement() { Size = new Vector2(400, 80); @@ -48,6 +51,7 @@ namespace osu.Game.Skinning.Components base.Update(); base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight); + base.Colour = Colour.Value; } } } diff --git a/osu.Game/Skinning/Components/PlayerName.cs b/osu.Game/Skinning/Components/PlayerName.cs index 21bf615bc6..70672a1f58 100644 --- a/osu.Game/Skinning/Components/PlayerName.cs +++ b/osu.Game/Skinning/Components/PlayerName.cs @@ -53,5 +53,7 @@ namespace osu.Game.Skinning.Components } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + + protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; } } diff --git a/osu.Game/Skinning/Components/TextElement.cs b/osu.Game/Skinning/Components/TextElement.cs index 936f6a529b..9d66c58ae8 100644 --- a/osu.Game/Skinning/Components/TextElement.cs +++ b/osu.Game/Skinning/Components/TextElement.cs @@ -36,5 +36,7 @@ namespace osu.Game.Skinning.Components } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + + protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; } } diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index 8f3a1d41c6..e3052aee5c 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; @@ -20,11 +21,16 @@ namespace osu.Game.Skinning [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.FontColour), nameof(SkinnableComponentStrings.FontColourDescription))] + public BindableColour4 FontColour { get; } = new BindableColour4(Colour4.White); + /// /// Implement to apply the user font selection to one or more components. /// protected abstract void SetFont(FontUsage font); + protected abstract void SetFontColour(Colour4 fontColour); + protected override void LoadComplete() { base.LoadComplete(); @@ -37,6 +43,8 @@ namespace osu.Game.Skinning FontUsage f = OsuFont.GetFont(e.NewValue, weight: fontWeight); SetFont(f); }, true); + + FontColour.BindValueChanged(e => SetFontColour(e.NewValue), true); } } } From 67f04f75a6c7a05c3135ea8328747669d4237624 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 15:26:27 +0200 Subject: [PATCH 231/308] Fix default color --- osu.Game/Overlays/Settings/SettingsColour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsColour.cs b/osu.Game/Overlays/Settings/SettingsColour.cs index a58c20adea..db248331d3 100644 --- a/osu.Game/Overlays/Settings/SettingsColour.cs +++ b/osu.Game/Overlays/Settings/SettingsColour.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings public partial class ColourControl : OsuClickableContainer, IHasPopover, IHasCurrentValue { - private readonly BindableWithCurrent current = new BindableWithCurrent(); + private readonly BindableWithCurrent current = new BindableWithCurrent(Colour4.White); public Bindable Current { From c77afe2a132e22162be34d3703076c2f57a63f34 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 16:04:42 +0200 Subject: [PATCH 232/308] Add tests --- .../Settings/TestSceneSettingsSource.cs | 28 +++++-- .../UserInterface/TestSceneSettingsColour.cs | 75 +++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index 309438e51c..f589a3baa1 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Overlays.Settings; @@ -19,16 +20,20 @@ namespace osu.Game.Tests.Visual.Settings { Children = new Drawable[] { - new FillFlowContainer + new PopoverContainer() { RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(20), - Width = 0.5f, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding(50), - ChildrenEnumerable = new TestTargetClass().CreateSettingsControls() + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Width = 0.5f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(50), + ChildrenEnumerable = new TestTargetClass().CreateSettingsControls() + }, }, }; } @@ -66,6 +71,13 @@ namespace osu.Game.Tests.Visual.Settings [SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))] public Bindable IntTextBoxBindable { get; } = new Bindable(); + + [SettingSource("Sample colour", "Change the colour", SettingControlType = typeof(SettingsColour))] + public BindableColour4 ColourBindable { get; } = new BindableColour4() + { + Default = Colour4.White, + Value = Colour4.Red + }; } private enum TestEnum diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs new file mode 100644 index 0000000000..d3de5a8319 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs @@ -0,0 +1,75 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays.Settings; +using osuTK.Graphics; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneSettingsColour : OsuManualInputManagerTestScene + { + private SettingsColour component; + + [Test] + public void TestColour() + { + createContent(); + + AddRepeatStep("set random colour", () => component.Current.Value = randomColour(), 4); + } + + [Test] + public void TestUserInteractions() + { + createContent(); + + AddStep("click colour", () => + { + InputManager.MoveMouseTo(component); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("colour picker spawned", () => this.ChildrenOfType().Any()); + } + + private void createContent() + { + Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + component = new SettingsColour + { + LabelText = "a sample component", + }, + }, + }, + }; + } + + private Colour4 randomColour() => new Color4( + RNG.NextSingle(), + RNG.NextSingle(), + RNG.NextSingle(), + 1); + } +} From 94c2f522ffa735bac22d91a07e0a92d04d9d5bff Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 17:31:33 +0200 Subject: [PATCH 233/308] Fix spacing --- osu.Game/Screens/Play/HUD/ArgonSongProgress.cs | 1 + osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 696369921a..3a4dc42484 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -29,6 +29,7 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 512edd7106..25d3c5588d 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -36,6 +36,7 @@ namespace osu.Game.Screens.Play.HUD [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))] public Bindable ShowTime { get; } = new BindableBool(true); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); From b86f246095fb79b9b67df4b0ec3b16155e997dd7 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 19:24:05 +0200 Subject: [PATCH 234/308] Fix code inspection failure --- osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs | 4 ++-- .../Visual/UserInterface/TestSceneSettingsColour.cs | 1 - osu.Game/Overlays/Settings/SettingsColour.cs | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index f589a3baa1..9544f77940 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Settings { Children = new Drawable[] { - new PopoverContainer() + new PopoverContainer { RelativeSizeAxes = Axes.Both, Child = new FillFlowContainer @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Settings public Bindable IntTextBoxBindable { get; } = new Bindable(); [SettingSource("Sample colour", "Change the colour", SettingControlType = typeof(SettingsColour))] - public BindableColour4 ColourBindable { get; } = new BindableColour4() + public BindableColour4 ColourBindable { get; } = new BindableColour4 { Default = Colour4.White, Value = Colour4.Red diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs index d3de5a8319..75ddacc110 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs @@ -3,7 +3,6 @@ #nullable disable -using System.Diagnostics.CodeAnalysis; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; diff --git a/osu.Game/Overlays/Settings/SettingsColour.cs b/osu.Game/Overlays/Settings/SettingsColour.cs index db248331d3..7a091f1a54 100644 --- a/osu.Game/Overlays/Settings/SettingsColour.cs +++ b/osu.Game/Overlays/Settings/SettingsColour.cs @@ -1,5 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. + using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; From 1a48b46536537eb050dc888b61c59e6c6e1eb851 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Thu, 19 Sep 2024 21:50:59 +0200 Subject: [PATCH 235/308] Fix test failures --- .../UserInterface/TestSceneSettingsColour.cs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs index 75ddacc110..6bed5f91c5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs @@ -45,24 +45,27 @@ namespace osu.Game.Tests.Visual.UserInterface private void createContent() { - Child = new PopoverContainer + AddStep("create component", () => { - RelativeSizeAxes = Axes.Both, - Child = new FillFlowContainer + Child = new PopoverContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 500, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer { - component = new SettingsColour + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - LabelText = "a sample component", + component = new SettingsColour + { + LabelText = "a sample component", + }, }, }, - }, - }; + }; + }); } private Colour4 randomColour() => new Color4( From 59ab71f786f13e258479769f21065adfcaadd234 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 20 Sep 2024 01:06:52 +0200 Subject: [PATCH 236/308] Implement minimum enclosing circle --- osu.Game/Utils/GeometryUtils.cs | 133 ++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 8572ac6609..7e6db10a28 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -218,5 +219,137 @@ namespace osu.Game.Utils return new[] { h.Position }; }); + + #region welzl_helpers + + // Function to check whether a point lies inside or on the boundaries of the circle + private static bool isInside((Vector2, float) c, Vector2 p) + { + return Precision.AlmostBigger(c.Item2, Vector2.Distance(c.Item1, p)); + } + + // Function to return a unique circle that intersects three points + private static (Vector2, float) circleFrom(Vector2 a, Vector2 b, Vector2 c) + { + if (Precision.AlmostEquals(0, (b.Y - a.Y) * (c.X - a.X) - (b.X - a.X) * (c.Y - a.Y))) + return circleFrom(a, b); + + // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2 + float d = 2 * (a.X * (b - c).Y + b.X * (c - a).Y + c.X * (a - b).Y); + float aSq = a.LengthSquared; + float bSq = b.LengthSquared; + float cSq = c.LengthSquared; + + var centre = new Vector2( + aSq * (b - c).Y + bSq * (c - a).Y + cSq * (a - b).Y, + aSq * (c - b).X + bSq * (a - c).X + cSq * (b - a).X) / d; + + return (centre, Vector2.Distance(a, centre)); + } + + // Function to return the smallest circle that intersects 2 points + private static (Vector2, float) circleFrom(Vector2 a, Vector2 b) + { + var centre = (a + b) / 2.0f; + return (centre, Vector2.Distance(a, b) / 2.0f); + } + + // Function to check whether a circle encloses the given points + private static bool isValidCircle((Vector2, float) c, ReadOnlySpan points) + { + // Iterating through all the points to check whether the points lie inside the circle or not + foreach (Vector2 p in points) + { + if (!isInside(c, p)) return false; + } + + return true; + } + + // Function to return the minimum enclosing circle for N <= 3 + private static (Vector2, float) minCircleTrivial(ReadOnlySpan points) + { + switch (points.Length) + { + case 0: + return (new Vector2(0, 0), 0); + + case 1: + return (points[0], 0); + + case 2: + return circleFrom(points[0], points[1]); + } + + // To check if MEC can be determined by 2 points only + for (int i = 0; i < 3; i++) + { + for (int j = i + 1; j < 3; j++) + { + var c = circleFrom(points[i], points[j]); + + if (isValidCircle(c, points)) + return c; + } + } + + return circleFrom(points[0], points[1], points[2]); + } + + // Returns the MEC using Welzl's algorithm + // Takes a set of input points P and a set R + // points on the circle boundary. + // n represents the number of points in P that are not yet processed. + private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n, Random random) + { + // Base case when all points processed or |R| = 3 + if (n == 0 || r.Length == 3) + return minCircleTrivial(r); + + // Pick a random point randomly + int idx = random.Next(n); + Vector2 p = points[idx]; + + // Put the picked point at the end of P since it's more efficient than + // deleting from the middle of the list + (points[idx], points[n - 1]) = (points[n - 1], points[idx]); + + // Get the MEC circle d from the set of points P - {p} + var d = welzlHelper(points, r, n - 1, random); + + // If d contains p, return d + if (isInside(d, p)) + return d; + + // Otherwise, must be on the boundary of the MEC + // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 + Span r2 = stackalloc Vector2[r.Length + 1]; + r.CopyTo(r2); + r2[r.Length] = p; + + // Return the MEC for P - {p} and R U {p} + return welzlHelper(points, r2, n - 1, random); + } + + #endregion + + /// + /// Function to find the minimum enclosing circle for a collection of points. + /// + /// A tuple containing the circle center and radius. + public static (Vector2, float) MinimumEnclosingCircle(IEnumerable points) + { + // Using Welzl's algorithm to find the minimum enclosing circle + // https://www.geeksforgeeks.org/minimum-enclosing-circle-using-welzls-algorithm/ + List pCopy = points.ToList(); + return welzlHelper(pCopy, Array.Empty(), pCopy.Count, new Random()); + } + + /// + /// Function to find the minimum enclosing circle for a collection of hit objects. + /// + /// A tuple containing the circle center and radius. + public static (Vector2, float) MinimumEnclosingCircle(IEnumerable hitObjects) => + MinimumEnclosingCircle(enumerateStartAndEndPositions(hitObjects)); } } From ee006247516569cb9fdd380d66612f21290d48ee Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 20 Sep 2024 01:07:47 +0200 Subject: [PATCH 237/308] use minimum enclosing circle selection centre in rotation --- .../Edit/OsuSelectionRotationHandler.cs | 9 ++++----- .../SkinEditor/SkinSelectionRotationHandler.cs | 9 ++++----- .../Compose/Components/SelectionBoxRotationHandle.cs | 10 +++++++--- .../Compose/Components/SelectionRotationHandler.cs | 6 ++++++ 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs index 62a39d3702..44d1543ae4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs @@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuHitObject[]? objectsInRotation; - private Vector2? defaultOrigin; private Dictionary? originalPositions; private Dictionary? originalPathControlPointPositions; @@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit changeHandler?.BeginChange(); objectsInRotation = selectedMovableObjects.ToArray(); - defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre; + DefaultOrigin = GeometryUtils.MinimumEnclosingCircle(objectsInRotation).Item1; originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position); originalPathControlPointPositions = objectsInRotation.OfType().ToDictionary( obj => obj, @@ -73,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Edit if (!OperationInProgress.Value) throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); - Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null); + Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && DefaultOrigin != null); - Vector2 actualOrigin = origin ?? defaultOrigin.Value; + Vector2 actualOrigin = origin ?? DefaultOrigin.Value; foreach (var ho in objectsInRotation) { @@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit objectsInRotation = null; originalPositions = null; originalPathControlPointPositions = null; - defaultOrigin = null; + DefaultOrigin = null; } private IEnumerable selectedMovableObjects => selectedItems.Cast() diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs index 36b38543d1..9fd28a1cad 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs @@ -46,7 +46,6 @@ namespace osu.Game.Overlays.SkinEditor private Drawable[]? objectsInRotation; - private Vector2? defaultOrigin; private Dictionary? originalRotations; private Dictionary? originalPositions; @@ -60,7 +59,7 @@ namespace osu.Game.Overlays.SkinEditor objectsInRotation = selectedItems.Cast().ToArray(); originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation); originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition)); - defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; + DefaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre; base.Begin(); } @@ -70,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor if (objectsInRotation == null) throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!"); - Debug.Assert(originalRotations != null && originalPositions != null && defaultOrigin != null); + Debug.Assert(originalRotations != null && originalPositions != null && DefaultOrigin != null); if (objectsInRotation.Length == 1 && origin == null) { @@ -79,7 +78,7 @@ namespace osu.Game.Overlays.SkinEditor return; } - var actualOrigin = origin ?? defaultOrigin.Value; + var actualOrigin = origin ?? DefaultOrigin.Value; foreach (var drawableItem in objectsInRotation) { @@ -100,7 +99,7 @@ namespace osu.Game.Overlays.SkinEditor objectsInRotation = null; originalPositions = null; originalRotations = null; - defaultOrigin = null; + DefaultOrigin = null; base.Commit(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index c62e0e0d41..898efc8b5e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -113,9 +113,13 @@ namespace osu.Game.Screens.Edit.Compose.Components private float convertDragEventToAngleOfRotation(DragEvent e) { - // Adjust coordinate system to the center of SelectionBox - float startAngle = MathF.Atan2(e.LastMousePosition.Y - selectionBox.DrawHeight / 2, e.LastMousePosition.X - selectionBox.DrawWidth / 2); - float endAngle = MathF.Atan2(e.MousePosition.Y - selectionBox.DrawHeight / 2, e.MousePosition.X - selectionBox.DrawWidth / 2); + // Adjust coordinate system to the center of the selection + Vector2 center = rotationHandler?.DefaultOrigin is not null + ? selectionBox.ToLocalSpace(rotationHandler.ToScreenSpace(rotationHandler.DefaultOrigin.Value)) + : selectionBox.DrawSize / 2; + + float startAngle = MathF.Atan2(e.LastMousePosition.Y - center.Y, e.LastMousePosition.X - center.X); + float endAngle = MathF.Atan2(e.MousePosition.Y - center.Y, e.MousePosition.X - center.X); return (endAngle - startAngle) * 180 / MathF.PI; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 532daaf7fa..680acad114 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -27,6 +27,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public Bindable CanRotateAroundPlayfieldOrigin { get; private set; } = new BindableBool(); + /// + /// Implementation-defined origin point to rotate around when no explicit origin is provided. + /// This field is only assigned during a rotation operation. + /// + public Vector2? DefaultOrigin { get; protected set; } + /// /// Performs a single, instant, atomic rotation operation. /// From 8e11cda41a35919cfddf5b6e601686b4f549b335 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Fri, 20 Sep 2024 01:07:54 +0200 Subject: [PATCH 238/308] use minimum enclosing circle selection centre in scale --- osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs | 2 +- osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 56c3ba9315..e9d5b3105a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -84,10 +84,10 @@ namespace osu.Game.Rulesets.Osu.Edit OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider ? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position)) : GeometryUtils.GetSurroundingQuad(objectsInScale.Keys); - defaultOrigin = OriginalSurroundingQuad.Value.Centre; originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2 ? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position)) : GeometryUtils.GetConvexHull(objectsInScale.Keys); + defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1; } public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs index 977aaade99..6915769212 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.SkinEditor objectsInScale = selectedItems.Cast().ToDictionary(d => d, d => new OriginalDrawableState(d)); OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray()))); - defaultOrigin = OriginalSurroundingQuad.Value.Centre; + defaultOrigin = ToLocalSpace(GeometryUtils.MinimumEnclosingCircle(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray())).Item1); isFlippedX = false; isFlippedY = false; From ec575e9de4a8a5ffc87afe58ea954443a3aa0ba3 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Fri, 20 Sep 2024 16:38:26 +0200 Subject: [PATCH 239/308] Rename Colour to AccentColour --- osu.Game/Screens/Play/HUD/ArgonSongProgress.cs | 4 ++-- osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs | 6 +++--- osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 4 ++-- osu.Game/Skinning/Components/BoxElement.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 3a4dc42484..1a18466743 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Play.HUD public Bindable ShowTime { get; } = new BindableBool(true); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] - public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); + public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); [Resolved] private Player? player { get; set; } @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Play.HUD base.Update(); content.Height = bar.Height + bar_height + info.Height; graphContainer.Height = bar.Height; - base.Colour = Colour.Value; + Colour = AccentColour.Value; } protected override void UpdateProgress(double progress, bool isIntro) diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs index 837e9547f0..fb2e93b62b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play.HUD public BindableBool InvertShear { get; } = new BindableBool(); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] - public new BindableColour4 Colour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF")); + public BindableColour4 AccentColour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF")); public ArgonWedgePiece() { @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.HUD InternalChild = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Colour.Value.Opacity(0.0f), Colour.Value.Opacity(0.25f)), + Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f)), }; } @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); - Colour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(Colour.Value.Opacity(0.0f), Colour.Value.Opacity(0.25f))); + AccentColour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f))); } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 25d3c5588d..93d75a22ba 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play.HUD public Bindable ShowTime { get; } = new BindableBool(true); [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] - public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); + public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); [Resolved] private Player? player { get; set; } @@ -119,7 +119,7 @@ namespace osu.Game.Screens.Play.HUD if (!Precision.AlmostEquals(Height, newHeight, 5f)) content.Height = newHeight; - base.Colour = Colour.Value; + Colour = AccentColour.Value; } private void updateBarVisibility() diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index e49ec0cc4d..633fb0c327 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -28,7 +28,7 @@ namespace osu.Game.Skinning.Components }; [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))] - public new BindableColour4 Colour { get; } = new BindableColour4(Colour4.White); + public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White); public BoxElement() { @@ -51,7 +51,7 @@ namespace osu.Game.Skinning.Components base.Update(); base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight); - base.Colour = Colour.Value; + Colour = AccentColour.Value; } } } From 73b6744a97ed3ca36db2c9ca99a1f451320f962c Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Fri, 20 Sep 2024 16:50:17 +0200 Subject: [PATCH 240/308] Rename FontColour to TextColour --- .../SkinComponents/SkinnableComponentStrings.cs | 8 ++++---- osu.Game/Skinning/Components/BeatmapAttributeText.cs | 2 +- osu.Game/Skinning/Components/PlayerName.cs | 2 +- osu.Game/Skinning/Components/TextElement.cs | 2 +- osu.Game/Skinning/FontAdjustableSkinComponent.cs | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index bd22527f67..33fda23cb0 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -70,14 +70,14 @@ namespace osu.Game.Localisation.SkinComponents public static LocalisableString ColourDescription => new TranslatableString(getKey(@"colour_description"), @"The colour of the component."); /// - /// "Font colour" + /// "Text colour" /// - public static LocalisableString FontColour => new TranslatableString(getKey(@"font_colour"), @"Font colour"); + public static LocalisableString TextColour => new TranslatableString(getKey(@"text_colour"), @"Text colour"); /// - /// "The colour of the font." + /// "The colour of the text." /// - public static LocalisableString FontColourDescription => new TranslatableString(getKey(@"font_colour_description"), @"The colour of the font."); + public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text."); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/Skinning/Components/BeatmapAttributeText.cs b/osu.Game/Skinning/Components/BeatmapAttributeText.cs index 06f0d9cea9..6e1d655cef 100644 --- a/osu.Game/Skinning/Components/BeatmapAttributeText.cs +++ b/osu.Game/Skinning/Components/BeatmapAttributeText.cs @@ -124,7 +124,7 @@ namespace osu.Game.Skinning.Components protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); - protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; + protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour; } // WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END. diff --git a/osu.Game/Skinning/Components/PlayerName.cs b/osu.Game/Skinning/Components/PlayerName.cs index 70672a1f58..5b6ded0cc5 100644 --- a/osu.Game/Skinning/Components/PlayerName.cs +++ b/osu.Game/Skinning/Components/PlayerName.cs @@ -54,6 +54,6 @@ namespace osu.Game.Skinning.Components protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); - protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; + protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour; } } diff --git a/osu.Game/Skinning/Components/TextElement.cs b/osu.Game/Skinning/Components/TextElement.cs index 9d66c58ae8..6e875c5590 100644 --- a/osu.Game/Skinning/Components/TextElement.cs +++ b/osu.Game/Skinning/Components/TextElement.cs @@ -37,6 +37,6 @@ namespace osu.Game.Skinning.Components protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); - protected override void SetFontColour(Colour4 fontColour) => text.Colour = fontColour; + protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour; } } diff --git a/osu.Game/Skinning/FontAdjustableSkinComponent.cs b/osu.Game/Skinning/FontAdjustableSkinComponent.cs index e3052aee5c..0821edf7fc 100644 --- a/osu.Game/Skinning/FontAdjustableSkinComponent.cs +++ b/osu.Game/Skinning/FontAdjustableSkinComponent.cs @@ -21,15 +21,15 @@ namespace osu.Game.Skinning [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] public Bindable Font { get; } = new Bindable(Typeface.Torus); - [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.FontColour), nameof(SkinnableComponentStrings.FontColourDescription))] - public BindableColour4 FontColour { get; } = new BindableColour4(Colour4.White); + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.TextColour), nameof(SkinnableComponentStrings.TextColourDescription))] + public BindableColour4 TextColour { get; } = new BindableColour4(Colour4.White); /// /// Implement to apply the user font selection to one or more components. /// protected abstract void SetFont(FontUsage font); - protected abstract void SetFontColour(Colour4 fontColour); + protected abstract void SetTextColour(Colour4 textColour); protected override void LoadComplete() { @@ -44,7 +44,7 @@ namespace osu.Game.Skinning SetFont(f); }, true); - FontColour.BindValueChanged(e => SetFontColour(e.NewValue), true); + TextColour.BindValueChanged(e => SetTextColour(e.NewValue), true); } } } From 8ceea9a5f7c80b6bc8d799380f9b70438f4dc65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 20 Sep 2024 17:19:38 +0200 Subject: [PATCH 241/308] Use scale origin when scaling sliders --- .../Edit/OsuSelectionScaleHandler.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs index 56c3ba9315..ea16946dcb 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs @@ -105,9 +105,7 @@ namespace osu.Game.Rulesets.Osu.Edit // is not looking to change the duration of the slider but expand the whole pattern. if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider) { - var originalInfo = objectsInScale[slider]; - Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null); - scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes, axisRotation); + scaleSlider(slider, scale, actualOrigin, objectsInScale[slider], axisRotation); } else { @@ -159,21 +157,25 @@ namespace osu.Game.Rulesets.Osu.Edit return scale; } - private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes, float axisRotation = 0) + private void scaleSlider(Slider slider, Vector2 scale, Vector2 origin, OriginalHitObjectState originalInfo, float axisRotation = 0) { + Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null); + scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON)); // Maintain the path types in case they were defaulted to bezier at some point during scaling for (int i = 0; i < slider.Path.ControlPoints.Count; i++) { - slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalPathPositions[i], axisRotation); - slider.Path.ControlPoints[i].Type = originalPathTypes[i]; + slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation); + slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i]; } // Snap the slider's length to the current beat divisor // to calculate the final resulting duration / bounding box before the final checks. slider.SnapTo(snapProvider); + slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation); + //if sliderhead or sliderend end up outside playfield, revert scaling. Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); (bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad); @@ -182,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Edit return; for (int i = 0; i < slider.Path.ControlPoints.Count; i++) - slider.Path.ControlPoints[i].Position = originalPathPositions[i]; + slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i]; + + slider.Position = originalInfo.Position; // Snap the slider's length again to undo the potentially-invalid length applied by the previous snap. slider.SnapTo(snapProvider); From 8bca8d60722039460f2446d5a083f151b4100e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Fri, 20 Sep 2024 17:38:49 +0200 Subject: [PATCH 242/308] Restore previous scale behavior when using scale popover --- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index dff370d259..ec347939e7 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -10,7 +10,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; using osuTK; @@ -35,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit private OsuCheckbox xCheckBox = null!; private OsuCheckbox yCheckBox = null!; + private BindableList selectedItems { get; } = new BindableList(); + public PreciseScalePopover(OsuSelectionScaleHandler scaleHandler, OsuGridToolboxGroup gridToolbox) { this.scaleHandler = scaleHandler; @@ -44,8 +49,10 @@ namespace osu.Game.Rulesets.Osu.Edit } [BackgroundDependencyLoader] - private void load() + private void load(EditorBeatmap editorBeatmap) { + selectedItems.BindTo(editorBeatmap.SelectedHitObjects); + Child = new FillFlowContainer { Width = 220, @@ -196,6 +203,7 @@ namespace osu.Game.Rulesets.Osu.Edit { ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value, ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, + ScaleOrigin.SelectionCentre when selectedItems.Count == 1 && selectedItems.First() is Slider slider => slider.Position, ScaleOrigin.SelectionCentre => null, _ => throw new ArgumentOutOfRangeException(nameof(scale)) }; From 59df9cbf0ff76e1bdf3d3b391600fe6444aeba71 Mon Sep 17 00:00:00 2001 From: Daniel Cios Date: Fri, 20 Sep 2024 18:07:26 +0200 Subject: [PATCH 243/308] Remove nullable disable --- .../Visual/UserInterface/TestSceneSettingsColour.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs index 6bed5f91c5..8d28116950 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsColour.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -19,14 +17,14 @@ namespace osu.Game.Tests.Visual.UserInterface { public partial class TestSceneSettingsColour : OsuManualInputManagerTestScene { - private SettingsColour component; + private SettingsColour? component; [Test] public void TestColour() { createContent(); - AddRepeatStep("set random colour", () => component.Current.Value = randomColour(), 4); + AddRepeatStep("set random colour", () => component!.Current.Value = randomColour(), 4); } [Test] @@ -36,7 +34,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click colour", () => { - InputManager.MoveMouseTo(component); + InputManager.MoveMouseTo(component!); InputManager.Click(MouseButton.Left); }); From 2dbbbe270daf475afeec30539af438d08de1956e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Sat, 21 Sep 2024 13:37:41 +0200 Subject: [PATCH 244/308] Scale around center when pressing alt while dragging selection box scale handle --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 7b0943c1d0..42e7b8c219 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -50,14 +50,14 @@ namespace osu.Game.Screens.Edit.Compose.Components rawScale = convertDragEventToScaleMultiplier(e); - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); } protected override bool OnKeyDown(KeyDownEvent e) { if (IsDragged) { - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); return true; } @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.OnKeyUp(e); if (IsDragged) - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); } protected override void OnDragEnd(DragEndEvent e) @@ -100,13 +100,13 @@ namespace osu.Game.Screens.Edit.Compose.Components if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y; } - private void applyScale(bool shouldLockAspectRatio) + private void applyScale(bool shouldLockAspectRatio, bool ignoreAnchor = false) { var newScale = shouldLockAspectRatio ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; - var scaleOrigin = originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); + Vector2? scaleOrigin = ignoreAnchor ? null : originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis()); } From 3180468db1001294266b2f59f1451802c6e2b1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Sat, 21 Sep 2024 14:22:17 +0200 Subject: [PATCH 245/308] Prevent the distance snap grid from being activated by alt key while dragging select box handle --- .../Edit/CatchHitObjectComposer.cs | 20 +++++++++++++++++++ .../Edit/OsuHitObjectComposer.cs | 2 ++ .../Edit/ComposerDistanceSnapProvider.cs | 17 +--------------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 83f48816f9..978aeba4ce 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -114,6 +114,26 @@ namespace osu.Game.Rulesets.Catch.Edit { } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) + return false; + + handleToggleViaKey(e); + return base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyUpEvent e) + { + handleToggleViaKey(e); + base.OnKeyUp(e); + } + + private void handleToggleViaKey(KeyboardEvent key) + { + DistanceSnapProvider.HandleToggleViaKey(key); + } + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) { var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 8fc2a9b7d3..c94dba6b23 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -369,6 +369,8 @@ namespace osu.Game.Rulesets.Osu.Edit gridSnapMomentary = shiftPressed; rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False; } + + DistanceSnapProvider.HandleToggleViaKey(key); } private DistanceSnapGrid createDistanceSnapGrid(IEnumerable selectedHitObjects) diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index b9850a94a3..979492fd8b 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -195,22 +195,7 @@ namespace osu.Game.Rulesets.Edit new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap }) }; - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) - return false; - - handleToggleViaKey(e); - return base.OnKeyDown(e); - } - - protected override void OnKeyUp(KeyUpEvent e) - { - handleToggleViaKey(e); - base.OnKeyUp(e); - } - - private void handleToggleViaKey(KeyboardEvent key) + public void HandleToggleViaKey(KeyboardEvent key) { bool altPressed = key.AltPressed; From 0077ba72ecac49a7b79916a76956c7dd02f89038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Sat, 21 Sep 2024 14:59:47 +0200 Subject: [PATCH 246/308] Freeze select box buttons in place as long as they are hovered --- .../Edit/Compose/Components/SelectionBox.cs | 26 +++++++++++++++++++ .../Compose/Components/SelectionBoxButton.cs | 9 +++++++ 2 files changed, 35 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 0cc8a8273f..39f0011a12 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -284,8 +285,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Action = action }; + button.OperationStarted += freezeButtonPosition; + button.HoverLost += unfreezeButtonPosition; + button.OperationStarted += operationStarted; button.OperationEnded += operationEnded; + buttons.Add(button); return button; @@ -357,8 +362,29 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted?.Invoke(); } + private Quad? frozenButtonsDrawQuad; + + private void freezeButtonPosition() + { + frozenButtonsDrawQuad = buttons.ScreenSpaceDrawQuad; + } + + private void unfreezeButtonPosition() + { + frozenButtonsDrawQuad = null; + } + private void ensureButtonsOnScreen() { + if (frozenButtonsDrawQuad != null) + { + buttons.Anchor = Anchor.TopLeft; + buttons.Origin = Anchor.TopLeft; + + buttons.Position = ToLocalSpace(frozenButtonsDrawQuad.Value.TopLeft) - new Vector2(button_padding); + return; + } + buttons.Position = Vector2.Zero; var thisQuad = ScreenSpaceDrawQuad; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs index 6108d44c81..e355add40b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxButton.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public Action? Action; + public event Action? HoverLost; + public SelectionBoxButton(IconUsage iconUsage, string tooltip) { this.iconUsage = iconUsage; @@ -61,6 +63,13 @@ namespace osu.Game.Screens.Edit.Compose.Components icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint); } + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + + HoverLost?.Invoke(); + } + public LocalisableString TooltipText { get; } } } From 1095f35025603ca1e948483e732d6d4346f6c51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Sat, 21 Sep 2024 15:25:37 +0200 Subject: [PATCH 247/308] Only store position instead of entire draw quad --- .../Screens/Edit/Compose/Components/SelectionBox.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 39f0011a12..4eae2b77f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -362,26 +361,26 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted?.Invoke(); } - private Quad? frozenButtonsDrawQuad; + private Vector2? frozenButtonsPosition; private void freezeButtonPosition() { - frozenButtonsDrawQuad = buttons.ScreenSpaceDrawQuad; + frozenButtonsPosition = buttons.ScreenSpaceDrawQuad.TopLeft; } private void unfreezeButtonPosition() { - frozenButtonsDrawQuad = null; + frozenButtonsPosition = null; } private void ensureButtonsOnScreen() { - if (frozenButtonsDrawQuad != null) + if (frozenButtonsPosition != null) { buttons.Anchor = Anchor.TopLeft; buttons.Origin = Anchor.TopLeft; - buttons.Position = ToLocalSpace(frozenButtonsDrawQuad.Value.TopLeft) - new Vector2(button_padding); + buttons.Position = ToLocalSpace(frozenButtonsPosition.Value) - new Vector2(button_padding); return; } From 1b77b3912baf0d991da00c01cd986b0439398cb5 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 22 Sep 2024 15:01:58 +0300 Subject: [PATCH 248/308] initial commit --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 3 +-- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 0899212b6c..f78a6b4703 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier, Mods = mods, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, - MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)), + MaxCombo = beatmap.GetMaxCombo(), }; return attributes; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index e93475ecff..c4fcd1f760 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -81,7 +81,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; - int maxCombo = beatmap.GetMaxCombo(); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int sliderCount = beatmap.HitObjects.Count(h => h is Slider); @@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, DrainRate = drainRate, - MaxCombo = maxCombo, + MaxCombo = beatmap.GetMaxCombo(), HitCircleCount = hitCirclesCount, SliderCount = sliderCount, SpinnerCount = spinnerCount, diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 28323693d0..6a1a047b7a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty ColourDifficulty = colourRating, PeakDifficulty = combinedRating, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, - MaxCombo = beatmap.HitObjects.Count(h => h is Hit), + MaxCombo = beatmap.GetMaxCombo(), }; return attributes; From 2995df7903c8c22bb14eec4ebf79ca6284a4f98e Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Sun, 22 Sep 2024 15:04:21 +0300 Subject: [PATCH 249/308] removed unnecessary includes --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs | 1 - osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index f78a6b4703..7d21409ee8 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6a1a047b7a..e3c550fbe9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Mods; -using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Scoring; namespace osu.Game.Rulesets.Taiko.Difficulty From 881c9dfbba2753c76aabba81948ec200b507b8fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 09:49:25 +0200 Subject: [PATCH 250/308] Fix score being cloned in async method causing random errors (again) Compare: https://github.com/ppy/osu/pull/24548. I don't have a reproduction scenario (judging from the stack trace of the crash it's likely to be nigh-impossible to concoct a reliable one), but there is some circumstantial evidence that this might help, namely that that previous fix above worked, and the pathway that's failing here is similarly async to the one that pull fixed. So I'm just gonna start with that and hope that it does the job. --- osu.Game/Screens/Play/SubmittingPlayer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index aea3bf6d5c..24c5b2c3d4 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -234,9 +234,12 @@ namespace osu.Game.Screens.Play { if (LoadedBeatmapSuccessfully) { + // compare: https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Play/Player.cs#L848-L851 + var scoreCopy = Score.DeepClone(); + Task.Run(async () => { - await submitScore(Score.DeepClone()).ConfigureAwait(false); + await submitScore(scoreCopy).ConfigureAwait(false); spectatorClient.EndPlaying(GameplayState); }).FireAndForget(); } From 92b5650ff8dab72c298a396960cb5ef51e1a5d3f Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 10:56:03 +0200 Subject: [PATCH 251/308] fix outdated comment --- .../Screens/Edit/Compose/Components/SelectionRotationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs index 680acad114..af3b3d6489 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionRotationHandler.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public partial class SelectionRotationHandler : Component { /// - /// Whether there is any ongoing scale operation right now. + /// Whether there is any ongoing rotation operation right now. /// public Bindable OperationInProgress { get; private set; } = new BindableBool(); From 0f758ca25f6d68a0b4a0c57bc3ea0e730d854172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 11:08:31 +0200 Subject: [PATCH 252/308] Continue displaying storyboard even if fully dimmed in specific circumstances Closes https://github.com/ppy/osu/issues/9315. Closes https://github.com/ppy/osu/issues/29867. Notably, this does nothing about https://github.com/ppy/osu/issues/25075, but I'm not sure what to do with that one in the first place. --- osu.Game/Screens/Play/DimmableStoryboard.cs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index 40cc0f66ad..84d99ea863 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -3,7 +3,9 @@ #nullable disable +using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -24,6 +26,21 @@ namespace osu.Game.Screens.Play private readonly Storyboard storyboard; private readonly IReadOnlyList mods; + /// + /// In certain circumstances, the storyboard cannot be hidden entirely even if it is fully dimmed. Such circumstances include: + /// + /// + /// cases where the storyboard has an overlay layer sprite, as it should continue to display fully dimmed + /// in front of the playfield (https://github.com/ppy/osu/issues/29867), + /// + /// + /// cases where the storyboard includes samples - as they are played back via drawable samples, + /// they must be present for the playback to occur (https://github.com/ppy/osu/issues/9315). + /// + /// + /// + private readonly Lazy storyboardMustAlwaysBePresent; + private DrawableStoryboard drawableStoryboard; /// @@ -38,6 +55,8 @@ namespace osu.Game.Screens.Play { this.storyboard = storyboard; this.mods = mods; + + storyboardMustAlwaysBePresent = new Lazy(() => storyboard.GetLayer(@"Overlay").Elements.Any() || storyboard.Layers.Any(l => l.Elements.OfType().Any())); } [BackgroundDependencyLoader] @@ -54,7 +73,7 @@ namespace osu.Game.Screens.Play base.LoadComplete(); } - protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowStoryboard.Value && DimLevel < 1); + protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowStoryboard.Value && (DimLevel < 1 || storyboardMustAlwaysBePresent.Value)); private void initializeStoryboard(bool async) { From a9ebfbe431e4616a7a0c3ea49182065839471014 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:37:42 +0200 Subject: [PATCH 253/308] Assert default origin not null in rotation handle --- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 1 + .../Edit/Compose/Components/SelectionBoxRotationHandle.cs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 30f397f518..2bf07d8e27 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -84,6 +84,7 @@ namespace osu.Game.Tests.Visual.Editing targetContainer = getTargetContainer(); initialRotation = targetContainer!.Rotation; + DefaultOrigin = ToLocalSpace(targetContainer.ToScreenSpace(Vector2.Zero)); base.Begin(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 898efc8b5e..03d600bfa2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -77,6 +77,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.OnDrag(e); + if (rotationHandler == null || !rotationHandler.OperationInProgress.Value) return; + rawCumulativeRotation += convertDragEventToAngleOfRotation(e); applyRotation(shouldSnap: e.ShiftPressed); @@ -114,9 +116,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private float convertDragEventToAngleOfRotation(DragEvent e) { // Adjust coordinate system to the center of the selection - Vector2 center = rotationHandler?.DefaultOrigin is not null - ? selectionBox.ToLocalSpace(rotationHandler.ToScreenSpace(rotationHandler.DefaultOrigin.Value)) - : selectionBox.DrawSize / 2; + Vector2 center = selectionBox.ToLocalSpace(rotationHandler!.ToScreenSpace(rotationHandler!.DefaultOrigin!.Value)); float startAngle = MathF.Atan2(e.LastMousePosition.Y - center.Y, e.LastMousePosition.X - center.X); float endAngle = MathF.Atan2(e.MousePosition.Y - center.Y, e.MousePosition.X - center.X); From 0d06b122c1630e277864118b9cde787747902a21 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:39:42 +0200 Subject: [PATCH 254/308] rename region --- osu.Game/Utils/GeometryUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 7e6db10a28..c933006cc5 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -220,7 +220,7 @@ namespace osu.Game.Utils return new[] { h.Position }; }); - #region welzl_helpers + #region Welzl helpers // Function to check whether a point lies inside or on the boundaries of the circle private static bool isInside((Vector2, float) c, Vector2 p) From 447d178e0104bc0fb03199a7c5af20918ea69cf2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:42:02 +0200 Subject: [PATCH 255/308] use named tuple members --- osu.Game/Utils/GeometryUtils.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index c933006cc5..51777f8ea0 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -223,9 +223,9 @@ namespace osu.Game.Utils #region Welzl helpers // Function to check whether a point lies inside or on the boundaries of the circle - private static bool isInside((Vector2, float) c, Vector2 p) + private static bool isInside((Vector2 Centre, float Radius) c, Vector2 p) { - return Precision.AlmostBigger(c.Item2, Vector2.Distance(c.Item1, p)); + return Precision.AlmostBigger(c.Radius, Vector2.Distance(c.Centre, p)); } // Function to return a unique circle that intersects three points @@ -336,7 +336,7 @@ namespace osu.Game.Utils /// /// Function to find the minimum enclosing circle for a collection of points. /// - /// A tuple containing the circle center and radius. + /// A tuple containing the circle centre and radius. public static (Vector2, float) MinimumEnclosingCircle(IEnumerable points) { // Using Welzl's algorithm to find the minimum enclosing circle @@ -348,7 +348,7 @@ namespace osu.Game.Utils /// /// Function to find the minimum enclosing circle for a collection of hit objects. /// - /// A tuple containing the circle center and radius. + /// A tuple containing the circle centre and radius. public static (Vector2, float) MinimumEnclosingCircle(IEnumerable hitObjects) => MinimumEnclosingCircle(enumerateStartAndEndPositions(hitObjects)); } From d0f12006a4e8755179fa9cd0faf979dab93ae526 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:42:28 +0200 Subject: [PATCH 256/308] update wikipedia url --- osu.Game/Utils/GeometryUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 51777f8ea0..8395c3a090 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -234,7 +234,7 @@ namespace osu.Game.Utils if (Precision.AlmostEquals(0, (b.Y - a.Y) * (c.X - a.X) - (b.X - a.X) * (c.Y - a.Y))) return circleFrom(a, b); - // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2 + // See: https://en.wikipedia.org/wiki/Circumcircle#Cartesian_coordinates float d = 2 * (a.X * (b - c).Y + b.X * (c - a).Y + c.X * (a - b).Y); float aSq = a.LengthSquared; float bSq = b.LengthSquared; From 40cfaabc53cc310809d91a89baebd0e279894bc0 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:43:36 +0200 Subject: [PATCH 257/308] verify n<=3 in minCircleTrivial --- osu.Game/Utils/GeometryUtils.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 8395c3a090..93991efa22 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -269,6 +269,9 @@ namespace osu.Game.Utils // Function to return the minimum enclosing circle for N <= 3 private static (Vector2, float) minCircleTrivial(ReadOnlySpan points) { + if (points.Length > 3) + throw new ArgumentException("Number of points must be at most 3", nameof(points)); + switch (points.Length) { case 0: From 42549e81aa9c750a6603c2e403ff403040a90c93 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 11:44:07 +0200 Subject: [PATCH 258/308] use RNG.Next --- osu.Game/Utils/GeometryUtils.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 93991efa22..d4968749bf 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -303,14 +303,14 @@ namespace osu.Game.Utils // Takes a set of input points P and a set R // points on the circle boundary. // n represents the number of points in P that are not yet processed. - private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n, Random random) + private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) { // Base case when all points processed or |R| = 3 if (n == 0 || r.Length == 3) return minCircleTrivial(r); // Pick a random point randomly - int idx = random.Next(n); + int idx = RNG.Next(n); Vector2 p = points[idx]; // Put the picked point at the end of P since it's more efficient than @@ -318,7 +318,7 @@ namespace osu.Game.Utils (points[idx], points[n - 1]) = (points[n - 1], points[idx]); // Get the MEC circle d from the set of points P - {p} - var d = welzlHelper(points, r, n - 1, random); + var d = welzlHelper(points, r, n - 1); // If d contains p, return d if (isInside(d, p)) @@ -331,7 +331,7 @@ namespace osu.Game.Utils r2[r.Length] = p; // Return the MEC for P - {p} and R U {p} - return welzlHelper(points, r2, n - 1, random); + return welzlHelper(points, r2, n - 1); } #endregion @@ -345,7 +345,7 @@ namespace osu.Game.Utils // Using Welzl's algorithm to find the minimum enclosing circle // https://www.geeksforgeeks.org/minimum-enclosing-circle-using-welzls-algorithm/ List pCopy = points.ToList(); - return welzlHelper(pCopy, Array.Empty(), pCopy.Count, new Random()); + return welzlHelper(pCopy, Array.Empty(), pCopy.Count); } /// From 4b349ba38738fbf862acccaeb43e6954782f6ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 11:53:32 +0200 Subject: [PATCH 259/308] Use cache for beatmap lookups on spectate screen @peppy noticed recently that attempting to spectate just a few users was very likely to end up in requests very quickly being rejected with code 429 ("Too Many Requests"). I'm somewhat certain that the reason for that is that a significant number of players is wont to retry a lot in quick succession. That means that spectator server is going to note a lot of gameplay start and end messages in quick succession, too. And as it turns out, every gameplay start would end up triggering a new beatmap set fetch request: https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Spectate/SpectatorScreen.cs#L131-L134 https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Play/SoloSpectatorScreen.cs#L168-L172 https://github.com/ppy/osu/blob/ccf1acce56798497edfaf92d3ece933469edcf0a/osu.Game/Screens/Play/SoloSpectatorScreen.cs#L243-L256 To attempt to curtail that, use the beatmap cache instead, which should prevent these unnecessary requests from firing in the first place, therefore reducing the chance of the client getting throttled. This technically means that a different endpoint is used to fetch the data (`GET /beatmaps/?ids[]=` rather than `GET /beatmapsets/lookup?beatmap_id={id}`), but docs claim that both should return the same data, and it looks to work fine in practice. --- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 95eb2d4376..269bc3bb92 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,12 +14,11 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Configuration; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; using osu.Game.Overlays; @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner { [Resolved] - private IAPIProvider api { get; set; } = null!; + private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; [Resolved] private PreviewTrackManager previewTrackManager { get; set; } = null!; @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Play /// private SpectatorGameplayState? immediateSpectatorGameplayState; - private GetBeatmapSetRequest? onlineBeatmapRequest; + private ScheduledDelegate? beatmapFetchCallback; private APIBeatmapSet? beatmapSet; @@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play private void clearDisplay() { watchButton.Enabled.Value = false; - onlineBeatmapRequest?.Cancel(); + beatmapFetchCallback?.Cancel(); beatmapPanelContainer.Clear(); previewTrackManager.StopAnyPlaying(this); } @@ -244,15 +244,17 @@ namespace osu.Game.Screens.Play { Debug.Assert(state.BeatmapID != null); - onlineBeatmapRequest = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); - onlineBeatmapRequest.Success += beatmapSet => Schedule(() => + beatmapLookupCache.GetBeatmapAsync(state.BeatmapID.Value).ContinueWith(t => beatmapFetchCallback = Schedule(() => { - this.beatmapSet = beatmapSet; - beatmapPanelContainer.Child = new BeatmapCardNormal(this.beatmapSet, allowExpansion: false); - checkForAutomaticDownload(); - }); + var beatmap = t.GetResultSafely(); - api.Queue(onlineBeatmapRequest); + if (beatmap?.BeatmapSet == null) + return; + + beatmapSet = beatmap.BeatmapSet; + beatmapPanelContainer.Child = new BeatmapCardNormal(beatmapSet, allowExpansion: false); + checkForAutomaticDownload(); + })); } private void checkForAutomaticDownload() From 86817d0cfc9be71adbd9f6ceb7ff369e880becd4 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 12:15:31 +0200 Subject: [PATCH 260/308] Add benchmark for minimum enclosing circle --- osu.Game.Benchmarks/BenchmarkGeometryUtils.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 osu.Game.Benchmarks/BenchmarkGeometryUtils.cs diff --git a/osu.Game.Benchmarks/BenchmarkGeometryUtils.cs b/osu.Game.Benchmarks/BenchmarkGeometryUtils.cs new file mode 100644 index 0000000000..2ab4d3369a --- /dev/null +++ b/osu.Game.Benchmarks/BenchmarkGeometryUtils.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using BenchmarkDotNet.Attributes; +using osu.Framework.Utils; +using osu.Game.Utils; +using osuTK; + +namespace osu.Game.Benchmarks +{ + public class BenchmarkGeometryUtils : BenchmarkTest + { + [Params(100, 1000, 2000, 4000, 8000, 10000)] + public int N; + + private Vector2[] points = null!; + + public override void SetUp() + { + points = new Vector2[N]; + + for (int i = 0; i < points.Length; ++i) + points[i] = new Vector2(RNG.Next(512), RNG.Next(384)); + } + + [Benchmark] + public void MinimumEnclosingCircle() => GeometryUtils.MinimumEnclosingCircle(points); + } +} From 203951780ed50a9ab3338548c9dd9bf32131f14e Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 12:15:42 +0200 Subject: [PATCH 261/308] use collection expression instead of stackalloc --- osu.Game/Utils/GeometryUtils.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index d4968749bf..e365a00862 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -325,13 +325,8 @@ namespace osu.Game.Utils return d; // Otherwise, must be on the boundary of the MEC - // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 - Span r2 = stackalloc Vector2[r.Length + 1]; - r.CopyTo(r2); - r2[r.Length] = p; - // Return the MEC for P - {p} and R U {p} - return welzlHelper(points, r2, n - 1); + return welzlHelper(points, [..r, p], n - 1); } #endregion From eead6b9eaea9aad7c5ed1b9afe6ef067de0afd3b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 13:13:33 +0200 Subject: [PATCH 262/308] return to stackalloc because its faster --- osu.Game/Utils/GeometryUtils.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index e365a00862..d4968749bf 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -325,8 +325,13 @@ namespace osu.Game.Utils return d; // Otherwise, must be on the boundary of the MEC + // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 + Span r2 = stackalloc Vector2[r.Length + 1]; + r.CopyTo(r2); + r2[r.Length] = p; + // Return the MEC for P - {p} and R U {p} - return welzlHelper(points, [..r, p], n - 1); + return welzlHelper(points, r2, n - 1); } #endregion From bf245aa9d61d2fc1f3cffede114f0ecd2a34a7e6 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 13:16:45 +0200 Subject: [PATCH 263/308] add a max depth to prevent stack overflow --- osu.Game/Utils/GeometryUtils.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index d4968749bf..c4c63903bb 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -305,8 +305,11 @@ namespace osu.Game.Utils // n represents the number of points in P that are not yet processed. private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) { + const int max_depth = 4000; + // Base case when all points processed or |R| = 3 - if (n == 0 || r.Length == 3) + // To prevent stack overflow, we stop at a certain depth and give an approximate answer + if (n == 0 || r.Length == 3 || points.Count - n >= max_depth) return minCircleTrivial(r); // Pick a random point randomly From 41826d0606d37e8d1b46bcad8e774d0b69af9521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 13:17:46 +0200 Subject: [PATCH 264/308] Add failing test case to demonstrate failure --- .../VolumeAwareHitSampleInfoTest.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/VolumeAwareHitSampleInfoTest.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/VolumeAwareHitSampleInfoTest.cs b/osu.Game.Rulesets.Taiko.Tests/VolumeAwareHitSampleInfoTest.cs new file mode 100644 index 0000000000..2b3a922067 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/VolumeAwareHitSampleInfoTest.cs @@ -0,0 +1,27 @@ +// 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.Game.Audio; +using osu.Game.Rulesets.Taiko.Skinning.Argon; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class VolumeAwareHitSampleInfoTest + { + [Test] + public void TestVolumeAwareHitSampleInfoIsNotEqualToItsUnderlyingSample( + [Values(HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP)] + string sample, + [Values(HitSampleInfo.BANK_NORMAL, HitSampleInfo.BANK_SOFT)] + string bank, + [Values(30, 70, 100)] int volume) + { + var underlyingSample = new HitSampleInfo(sample, bank, volume: volume); + var volumeAwareSample = new VolumeAwareHitSampleInfo(underlyingSample); + + Assert.That(underlyingSample, Is.Not.EqualTo(volumeAwareSample)); + } + } +} From e8a394f89485e61b37c61f095c7c9ed1c5c3b121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 13:27:36 +0200 Subject: [PATCH 265/308] Fix argon volume-aware hitsounds not correctly playing immediately after object placement Closes https://github.com/ppy/osu/issues/29832. The underlying reason for the incorrect sample playback was an equality comparer failure. Samples are contained in several pools which are managed by the playfield. In particular, the pools are keyed by `ISampleInfo` instances. This means that for correct operation, `ISampleInfo` has to implement `IEquatable` and also provide an appropriately correct `GetHashCode()` implementation. Different audible samples must not compare equal to each other when represented by `ISampleInfo`. As it turns out, `VolumeAwareHitSampleInfo` failed on this, due to not overriding equality members. Therefore, a `new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL, volume: 70)` was allowed to compare equal to a `VolumeAwareHitSampleInfo` wrapping it, *even though they correspond to completely different sounds and go through entirely different lookup path sequences*. Therefore, to fix, provide more proper equality implementations for `VolumeAwareHitSampleInfo`. When testing note that this issue *only occurs immediately after placing an object*. Saving and re-entering editor makes this issue go away. I haven't looked too long into why, but the general gist of it is ordering; it appears that a `normal-hitnormal` pool exists at point of query of a new object placement, but does not seem to exist when entering editor afresh. That said I'm not sure that ordering aspect of this bug matters much if at all, since the two `IHitSampleInfo`s should never be allowed to alias with each other at all wrt equality. --- .../Argon/VolumeAwareHitSampleInfo.cs | 20 +++++++++++++++++++ osu.Game/Audio/HitSampleInfo.cs | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs index 3ca4b5a3c7..288ffde052 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/VolumeAwareHitSampleInfo.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Game.Audio; @@ -48,5 +49,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return originalBank; } } + + public override bool Equals(HitSampleInfo? other) => other is VolumeAwareHitSampleInfo && base.Equals(other); + + /// + /// + /// This override attempts to match the override above, but in theory it is not strictly necessary. + /// Recall that must meet the following requirements: + /// + /// + /// "If two objects compare as equal, the method for each object must return the same value. + /// However, if two objects do not compare as equal, methods for the two objects do not have to return different values." + /// + /// + /// Making this override combine the value generated by the base implementation with a constant means + /// that and instances which have the same values of their members + /// will not have equal hash codes, which is slightly more efficient when these objects are used as dictionary keys. + /// + /// + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), 1); } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index f9c93d72ff..ce5e217532 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -96,7 +96,7 @@ namespace osu.Game.Audio public virtual HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) => new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume)); - public bool Equals(HitSampleInfo? other) + public virtual bool Equals(HitSampleInfo? other) => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; public override bool Equals(object? obj) From d6c17f6ac0b08b5a5a4ba541b82fc300301d5918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Sep 2024 14:29:56 +0200 Subject: [PATCH 266/308] Implement "form" dropdown control --- .../UserInterface/TestSceneFormControls.cs | 6 + .../Graphics/UserInterfaceV2/FormDropdown.cs | 251 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index b456da0f26..89b4ae9f97 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; @@ -94,6 +95,11 @@ namespace osu.Game.Tests.Visual.UserInterface Instantaneous = false, TabbableContentContainer = this, }, + new FormEnumDropdown + { + Caption = EditorSetupStrings.EnableCountdown, + HintText = EditorSetupStrings.CountdownDescription, + }, }, }, } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs b/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs new file mode 100644 index 0000000000..d47b9ac73d --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormDropdown.cs @@ -0,0 +1,251 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormDropdown : OsuDropdown + { + /// + /// Caption describing this slider bar, displayed on top of the controls. + /// + public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption. + /// + public LocalisableString HintText { get; init; } + + private FormDropdownHeader header = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + + header.Caption = Caption; + header.HintText = HintText; + } + + protected override DropdownHeader CreateHeader() => header = new FormDropdownHeader + { + Dropdown = this, + }; + + protected override DropdownMenu CreateMenu() => new FormDropdownMenu(); + + private partial class FormDropdownHeader : DropdownHeader + { + public FormDropdown Dropdown { get; set; } = null!; + + protected override DropdownSearchBar CreateSearchBar() => SearchBar = new FormDropdownSearchBar(); + + private LocalisableString captionText; + private LocalisableString hintText; + private LocalisableString labelText; + + public LocalisableString Caption + { + get => captionText; + set + { + captionText = value; + + if (caption.IsNotNull()) + caption.Caption = value; + } + } + + public LocalisableString HintText + { + get => hintText; + set + { + hintText = value; + + if (caption.IsNotNull()) + caption.TooltipText = value; + } + } + + protected override LocalisableString Label + { + get => labelText; + set + { + labelText = value; + + if (label.IsNotNull()) + label.Text = labelText; + } + } + + protected new FormDropdownSearchBar SearchBar { get; set; } = null!; + + private FormFieldCaption caption = null!; + private OsuSpriteText label = null!; + private SpriteIcon chevron = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.None; + Height = 50; + + Masking = true; + CornerRadius = 5; + + Foreground.AutoSizeAxes = Axes.None; + Foreground.RelativeSizeAxes = Axes.Both; + Foreground.Padding = new MarginPadding(9); + Foreground.Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + label = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + chevron = new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronDown, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(16), + }, + }; + + AddInternal(new HoverClickSounds()); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Dropdown.Current.BindDisabledChanged(_ => updateState()); + SearchBar.SearchTerm.BindValueChanged(_ => updateState(), true); + Dropdown.Menu.StateChanged += _ => + { + updateState(); + updateChevron(); + }; + SearchBar.TextBox.OnCommit += (_, _) => + { + Background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + private void updateState() + { + label.Alpha = string.IsNullOrEmpty(SearchBar.SearchTerm.Value) ? 1 : 0; + + caption.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + label.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + chevron.Colour = Dropdown.Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + DisabledColour = Colour4.White; + + bool dropdownOpen = Dropdown.Menu.State == MenuState.Open; + + if (!Dropdown.Current.Disabled) + { + BorderThickness = IsHovered || dropdownOpen ? 2 : 0; + BorderColour = dropdownOpen ? colourProvider.Highlight1 : colourProvider.Light4; + + if (dropdownOpen) + Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + Background.Colour = colourProvider.Background5; + } + else + { + Background.Colour = colourProvider.Background4; + } + } + + private void updateChevron() + { + bool open = Dropdown.Menu.State == MenuState.Open; + chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint); + } + } + + private partial class FormDropdownSearchBar : DropdownSearchBar + { + public FormTextBox.InnerTextBox TextBox { get; private set; } = null!; + + protected override void PopIn() => this.FadeIn(); + protected override void PopOut() => this.FadeOut(); + + protected override TextBox CreateTextBox() => TextBox = new FormTextBox.InnerTextBox(); + + [BackgroundDependencyLoader] + private void load() + { + TextBox.Anchor = Anchor.BottomLeft; + TextBox.Origin = Anchor.BottomLeft; + TextBox.RelativeSizeAxes = Axes.X; + TextBox.Margin = new MarginPadding(9); + } + } + + private partial class FormDropdownMenu : OsuDropdownMenu + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + ItemsContainer.Padding = new MarginPadding(9); + Margin = new MarginPadding { Top = 5 }; + + MaskingContainer.BorderThickness = 2; + MaskingContainer.BorderColour = colourProvider.Highlight1; + } + } + } + + public partial class FormEnumDropdown : FormDropdown + where T : struct, Enum + { + public FormEnumDropdown() + { + Items = Enum.GetValues(); + } + } +} From c857de3a9a45f691235a1ac9d4ddd5381e6e5042 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 24 Sep 2024 11:44:02 +0200 Subject: [PATCH 267/308] Revert "add a max depth to prevent stack overflow" This reverts commit bf245aa9d61d2fc1f3cffede114f0ecd2a34a7e6. --- osu.Game/Utils/GeometryUtils.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index c4c63903bb..d4968749bf 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -305,11 +305,8 @@ namespace osu.Game.Utils // n represents the number of points in P that are not yet processed. private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) { - const int max_depth = 4000; - // Base case when all points processed or |R| = 3 - // To prevent stack overflow, we stop at a certain depth and give an approximate answer - if (n == 0 || r.Length == 3 || points.Count - n >= max_depth) + if (n == 0 || r.Length == 3) return minCircleTrivial(r); // Pick a random point randomly From 86432078dd04ea69de005fac9f98c1353a59905d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 11:53:02 +0200 Subject: [PATCH 268/308] Remove usage of switch expression syntax It's not universally accepted here and a `when` crept in that can be bypassed entirely using rather clean baseline language constructs, so why bother at this point. --- .../Edit/PreciseScalePopover.cs | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ec347939e7..33b0c14185 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -198,15 +198,26 @@ namespace osu.Game.Rulesets.Osu.Edit updateAxisCheckBoxesEnabled(); } - private Vector2? getOriginPosition(PreciseScaleInfo scale) => - scale.Origin switch + private Vector2? getOriginPosition(PreciseScaleInfo scale) + { + switch (scale.Origin) { - ScaleOrigin.GridCentre => gridToolbox.StartPosition.Value, - ScaleOrigin.PlayfieldCentre => OsuPlayfield.BASE_SIZE / 2, - ScaleOrigin.SelectionCentre when selectedItems.Count == 1 && selectedItems.First() is Slider slider => slider.Position, - ScaleOrigin.SelectionCentre => null, - _ => throw new ArgumentOutOfRangeException(nameof(scale)) - }; + case ScaleOrigin.GridCentre: + return gridToolbox.StartPosition.Value; + + case ScaleOrigin.PlayfieldCentre: + return OsuPlayfield.BASE_SIZE / 2; + + case ScaleOrigin.SelectionCentre: + if (selectedItems.Count == 1 && selectedItems.First() is Slider slider) + return slider.Position; + + return null; + + default: + throw new ArgumentOutOfRangeException(nameof(scale)); + } + } private Axes getAdjustAxis(PreciseScaleInfo scale) => scale.XAxis ? scale.YAxis ? Axes.Both : Axes.X : Axes.Y; From 3031b68552cc9b2caa498a92ca4c72c4711a8871 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 24 Sep 2024 11:56:04 +0200 Subject: [PATCH 269/308] add TestMinimumEnclosingCircle --- osu.Game.Tests/Utils/GeometryUtilsTest.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Utils/GeometryUtilsTest.cs b/osu.Game.Tests/Utils/GeometryUtilsTest.cs index ded4656ac1..f73175bb5b 100644 --- a/osu.Game.Tests/Utils/GeometryUtilsTest.cs +++ b/osu.Game.Tests/Utils/GeometryUtilsTest.cs @@ -29,5 +29,23 @@ namespace osu.Game.Tests.Utils Assert.That(hull, Is.EquivalentTo(expectedPoints)); } + + [TestCase(new int[] { }, 0, 0, 0)] + [TestCase(new[] { 0, 0 }, 0, 0, 0)] + [TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, 1, 0, 1)] + [TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, 1, 0, 1)] + [TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, 3, 4.5f, 5.5901699f)] + public void TestMinimumEnclosingCircle(int[] values, float x, float y, float r) + { + var points = new Vector2[values.Length / 2]; + for (int i = 0; i < values.Length; i += 2) + points[i / 2] = new Vector2(values[i], values[i + 1]); + + (var centre, float radius) = GeometryUtils.MinimumEnclosingCircle(points); + + Assert.That(centre.X, Is.EqualTo(x).Within(0.0001)); + Assert.That(centre.Y, Is.EqualTo(y).Within(0.0001)); + Assert.That(radius, Is.EqualTo(r).Within(0.0001)); + } } } From b54b4063bece8eac19e4364773c2ab842fafc636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 24 Sep 2024 12:40:28 +0200 Subject: [PATCH 270/308] Rename parameter --- .../Edit/Compose/Components/SelectionBoxScaleHandle.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs index 42e7b8c219..3b7e29cf3d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxScaleHandle.cs @@ -50,14 +50,14 @@ namespace osu.Game.Screens.Edit.Compose.Components rawScale = convertDragEventToScaleMultiplier(e); - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed); } protected override bool OnKeyDown(KeyDownEvent e) { if (IsDragged) { - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed); return true; } @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components base.OnKeyUp(e); if (IsDragged) - applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, ignoreAnchor: e.AltPressed); + applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed); } protected override void OnDragEnd(DragEndEvent e) @@ -100,13 +100,13 @@ namespace osu.Game.Screens.Edit.Compose.Components if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y; } - private void applyScale(bool shouldLockAspectRatio, bool ignoreAnchor = false) + private void applyScale(bool shouldLockAspectRatio, bool useDefaultOrigin = false) { var newScale = shouldLockAspectRatio ? new Vector2((rawScale.X + rawScale.Y) * 0.5f) : rawScale; - Vector2? scaleOrigin = ignoreAnchor ? null : originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); + Vector2? scaleOrigin = useDefaultOrigin ? null : originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value); scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis()); } From 4c2ebdb2dbb3250b4f05d98bfda869e341916519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 12:53:54 +0200 Subject: [PATCH 271/308] Simplify accent colour assignment in argon wedge piece --- osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs index fb2e93b62b..46a658cd1c 100644 --- a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.Play.HUD InternalChild = new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f)), }; } @@ -50,7 +49,7 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); - AccentColour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f))); + AccentColour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f)), true); } } } From 3ad734296473eae7fcfd91b3ed11b43fcc0d4774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 24 Sep 2024 13:35:56 +0200 Subject: [PATCH 272/308] Add tests for shift and alt modifiers in select box --- .../Editing/TestSceneComposerSelection.cs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 3884a3108f..3d7aef5a65 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; @@ -36,6 +37,9 @@ namespace osu.Game.Tests.Visual.Editing private ContextMenuContainer contextMenuContainer => Editor.ChildrenOfType().First(); + private SelectionBoxScaleHandle getScaleHandle(Anchor anchor) + => Editor.ChildrenOfType().First(it => it.Anchor == anchor); + private void moveMouseToObject(Func targetFunc) { AddStep("move mouse to object", () => @@ -519,5 +523,137 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); } + + [Test] + public void TestShiftModifierMaintainsAspectRatio() + { + HitCircle[] addedObjects = null!; + + float aspectRatioBeforeDrag = 0; + + float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y); + + AddStep("add hitobjects", () => + { + EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100, Position = new Vector2(150, 150) }, + new HitCircle { StartTime = 200, Position = new Vector2(250, 200) }, + }); + + aspectRatioBeforeDrag = getAspectRatio(); + }); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre)); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + + AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio())); + + AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft)); + + AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio())); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + } + + [Test] + public void TestAltModifierScalesAroundCenter() + { + HitCircle[] addedObjects = null!; + + Vector2 centerBeforeDrag = Vector2.Zero; + + Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2; + + AddStep("add hitobjects", () => + { + EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100, Position = new Vector2(150, 150) }, + new HitCircle { StartTime = 200, Position = new Vector2(250, 200) }, + }); + + centerBeforeDrag = getCenter(); + }); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre)); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + + AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter())); + + AddStep("press alt", () => InputManager.PressKey(Key.AltLeft)); + + AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter())); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); + } + + [Test] + public void TestShiftAndAltModifierKeys() + { + HitCircle[] addedObjects = null!; + + float aspectRatioBeforeDrag = 0; + + Vector2 centerBeforeDrag = Vector2.Zero; + + float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y); + + Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2; + + AddStep("add hitobjects", () => + { + EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100, Position = new Vector2(150, 150) }, + new HitCircle { StartTime = 200, Position = new Vector2(250, 200) }, + }); + + aspectRatioBeforeDrag = getAspectRatio(); + centerBeforeDrag = getCenter(); + }); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre)); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + + AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio())); + + AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter())); + + AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft)); + + AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio())); + + AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter())); + + AddStep("press alt", () => InputManager.PressKey(Key.AltLeft)); + + AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter())); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + + AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft)); + } } } From 15c4b1dc8f81dd4db100854cde33f928db6307ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 24 Sep 2024 13:45:03 +0200 Subject: [PATCH 273/308] Move mouse horizontally in test to make sure it doesn't accidentally maintain aspect ratio --- osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 3d7aef5a65..cbc9088d04 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -550,7 +550,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0))); AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio())); @@ -589,7 +589,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0))); AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter())); @@ -633,7 +633,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); - AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50))); + AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0))); AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio())); From 7f8b64bb6db05cacb52a1224a2d07c4fd4b9b5f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Aug 2024 15:59:42 +0200 Subject: [PATCH 274/308] Redesign directory & file selector (and update usages accordingly) --- .../Settings/TestSceneDirectorySelector.cs | 5 ++ .../Visual/Settings/TestSceneFileSelector.cs | 34 +++++--- .../Screens/Setup/StablePathSelectScreen.cs | 3 + .../UserInterfaceV2/OsuDirectorySelector.cs | 43 ++++++++-- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 79 ++++++++++++++++--- .../OsuDirectorySelectorDirectory.cs | 31 +------- .../OsuDirectorySelectorHiddenToggle.cs | 3 +- .../OsuDirectorySelectorParentDirectory.cs | 8 ++ .../UserInterfaceV2/OsuFileSelector.cs | 52 +++++++++--- .../FirstRunSetup/ScreenImportFromStable.cs | 8 ++ .../Maintenance/DirectorySelectScreen.cs | 7 +- .../Screens/Edit/Setup/LabelledFileChooser.cs | 9 +++ osu.Game/Screens/Import/FileImportScreen.cs | 12 +-- 13 files changed, 220 insertions(+), 74 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs index 3ef0ffc13a..03ecd4af61 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -9,6 +9,11 @@ namespace osu.Game.Tests.Visual.Settings { public partial class TestSceneDirectorySelector : ThemeComparisonTestScene { + public TestSceneDirectorySelector() + : base(false) + { + } + protected override Drawable CreateContent() => new OsuDirectorySelector { RelativeSizeAxes = Axes.Both diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs index c70277987e..cf8a589152 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs @@ -1,37 +1,49 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osu.Game.Tests.Visual.UserInterface; namespace osu.Game.Tests.Visual.Settings { public partial class TestSceneFileSelector : ThemeComparisonTestScene { - [Resolved] - private OsuColour colours { get; set; } = null!; + public TestSceneFileSelector() + : base(false) + { + } [Test] public void TestJpgFilesOnly() { AddStep("create", () => { - ContentContainer.Children = new Drawable[] + var colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + ContentContainer.Child = new DependencyProvidingContainer { - new Box + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoam + (typeof(OverlayColourProvider), colourProvider) }, - new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3 + }, + new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) + { + RelativeSizeAxes = Axes.Both, + }, + } }; }); } diff --git a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs index 74404e06f8..91b03ed085 100644 --- a/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs @@ -27,6 +27,9 @@ namespace osu.Game.Tournament.Screens.Setup [Resolved] private MatchIPCInfo ipc { get; set; } = null!; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private OsuDirectorySelector directorySelector = null!; private DialogOverlay? overlay; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs index 21f926ba42..4002480e7f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs @@ -7,14 +7,18 @@ using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class OsuDirectorySelector : DirectorySelector { - public const float ITEM_HEIGHT = 20; + public const float ITEM_HEIGHT = 16; + + private Box hiddenToggleBackground = null!; public OsuDirectorySelector(string initialPath = null) : base(initialPath) @@ -22,16 +26,45 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { - Padding = new MarginPadding(10); + AddInternal(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + Depth = float.MaxValue, + }); + + hiddenToggleBackground.Colour = colourProvider.Background4; } - protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer + { + Padding = new MarginPadding + { + Horizontal = 20, + Vertical = 15, + } + }; protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay(); - protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } }; + protected override Drawable CreateHiddenToggleButton() => new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + hiddenToggleBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new OsuDirectorySelectorHiddenToggle + { + Current = { BindTarget = ShowHiddenItems }, + }, + } + }; protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs index 0917b9db97..5b52663198 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs @@ -6,28 +6,48 @@ using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { internal partial class OsuDirectorySelectorBreadcrumbDisplay : DirectorySelectorBreadcrumbDisplay { - protected override Drawable CreateCaption() => new OsuSpriteText + public const float HEIGHT = 45; + public const float HORIZONTAL_PADDING = 20; + + protected override Drawable CreateCaption() => Empty().With(d => { - Text = "Current Directory: ", - Font = OsuFont.Default.With(size: OsuDirectorySelector.ITEM_HEIGHT), - }; + d.Origin = Anchor.CentreLeft; + d.Anchor = Anchor.CentreLeft; + d.Alpha = 0; + }); protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayComputer(); protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName); - public OsuDirectorySelectorBreadcrumbDisplay() + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - Padding = new MarginPadding(15); + ((FillFlowContainer)InternalChild).Padding = new MarginPadding + { + Horizontal = HORIZONTAL_PADDING, + Vertical = 10, + }; + + AddInternal(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + Depth = 1, + }); } private partial class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory @@ -40,26 +60,67 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - private partial class OsuBreadcrumbDisplayDirectory : OsuDirectorySelectorDirectory + private partial class OsuBreadcrumbDisplayDirectory : DirectorySelectorDirectory { public OsuBreadcrumbDisplayDirectory(DirectoryInfo directory, string displayName = null) : base(directory, displayName) { } + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + [BackgroundDependencyLoader] private void load() { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + Flow.AutoSizeAxes = Axes.X; + Flow.Height = 25; + Flow.Margin = new MarginPadding { Horizontal = 10, }; + + AddRangeInternal(new Drawable[] + { + new Background + { + Depth = 1 + }, + new HoverClickSounds(), + }); + Flow.Add(new SpriteIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Icon = FontAwesome.Solid.ChevronRight, - Size = new Vector2(FONT_SIZE / 2) + Size = new Vector2(FONT_SIZE / 2), + Margin = new MarginPadding { Left = 5, }, }); + Flow.Colour = colourProvider.Light3; } - protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? base.Icon : null; + protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); + + protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? FontAwesome.Solid.Database : null; + + internal partial class Background : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColourProvider) + { + RelativeSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 5; + + InternalChild = new Box + { + Colour = overlayColourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }; + } + } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs index 932017b03e..a1d76dd7e3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs @@ -6,13 +6,10 @@ using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -24,43 +21,23 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; AddRangeInternal(new Drawable[] { - new Background - { - Depth = 1 - }, new HoverClickSounds() }); + + Colour = colours.Orange1; } - protected override SpriteText CreateSpriteText() => new OsuSpriteText(); + protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.Bold)); protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? FontAwesome.Solid.Database : FontAwesome.Regular.Folder; - - internal partial class Background : CompositeDrawable - { - [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider overlayColourProvider, OsuColour colours) - { - RelativeSizeAxes = Axes.Both; - - Masking = true; - CornerRadius = 5; - - InternalChild = new Box - { - Colour = overlayColourProvider?.Background5 ?? colours.GreySeaFoamDarker, - RelativeSizeAxes = Axes.Both, - }; - } - } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs index 7665ed507f..ffedc9386f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs @@ -16,7 +16,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.None; AutoSizeAxes = Axes.None; - Size = new Vector2(100, 50); + Size = new Vector2(100, OsuDirectorySelectorBreadcrumbDisplay.HEIGHT); + Margin = new MarginPadding { Right = OsuDirectorySelectorBreadcrumbDisplay.HORIZONTAL_PADDING, }; Anchor = Anchor.CentreLeft; Origin = Anchor.CentreLeft; LabelTextFlowContainer.Anchor = Anchor.CentreLeft; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs index c0ac9f21ca..d274a0ecfe 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -14,5 +16,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 : base(directory, "..") { } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Content1; + } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index 7097102335..feedeb8ff3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -8,32 +8,64 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { public partial class OsuFileSelector : FileSelector { + private Box hiddenToggleBackground = null!; + public OsuFileSelector(string initialPath = null, string[] validFileExtensions = null) - : base(initialPath, validFileExtensions) { } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { - Padding = new MarginPadding(10); + AddInternal(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + Depth = float.MaxValue, + }); + + hiddenToggleBackground.Colour = colourProvider.Background4; } - protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer + { + Padding = new MarginPadding + { + Horizontal = 20, + Vertical = 15, + } + }; protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay(); - protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } }; + protected override Drawable CreateHiddenToggleButton() => new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + hiddenToggleBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new OsuDirectorySelectorHiddenToggle + { + Current = { BindTarget = ShowHiddenItems }, + }, + } + }; protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); @@ -51,19 +83,17 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; AddRangeInternal(new Drawable[] { - new OsuDirectorySelectorDirectory.Background - { - Depth = 1 - }, new HoverClickSounds() }); + + Colour = colourProvider.Light3; } protected override IconUsage? Icon @@ -91,7 +121,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - protected override SpriteText CreateSpriteText() => new OsuSpriteText(); + protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); } } } diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs index 983cb0bbb4..5eb38b6e11 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenImportFromStable.cs @@ -314,6 +314,7 @@ namespace osu.Game.Overlays.FirstRunSetup private partial class DirectoryChooserPopover : OsuPopover { public DirectoryChooserPopover(Bindable currentDirectory) + : base(false) { Child = new Container { @@ -325,6 +326,13 @@ namespace osu.Game.Overlays.FirstRunSetup }, }; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Body.BorderColour = colourProvider.Highlight1; + Body.BorderThickness = 2; + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs index e87ca32bf6..f6f8d3b336 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/DirectorySelectScreen.cs @@ -48,8 +48,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance /// protected virtual DirectoryInfo InitialPath => null; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { InternalChild = new Container { @@ -64,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.GreySeaFoamDark + Colour = colourProvider.Background4, }, new GridContainer { diff --git a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs index 61f33c4bdc..a113ca5407 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs @@ -18,6 +18,7 @@ using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Edit.Setup @@ -118,6 +119,7 @@ namespace osu.Game.Screens.Edit.Setup protected override string PopOutSampleName => "UI/overlay-big-pop-out"; public FileChooserPopover(string[] handledExtensions, Bindable currentFile, string? chooserPath) + : base(false) { Child = new Container { @@ -129,6 +131,13 @@ namespace osu.Game.Screens.Edit.Setup }, }; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Body.BorderColour = colourProvider.Highlight1; + Body.BorderThickness = 2; + } } } } diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 6b7a269d12..1bdacae87f 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -15,6 +15,7 @@ using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Screens.Import @@ -36,8 +37,8 @@ namespace osu.Game.Screens.Import [Resolved] private OsuGameBase game { get; set; } - [Resolved] - private OsuColour colours { get; set; } + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); [BackgroundDependencyLoader(true)] private void load() @@ -52,11 +53,6 @@ namespace osu.Game.Screens.Import Size = new Vector2(0.9f, 0.8f), Children = new Drawable[] { - new Box - { - Colour = colours.GreySeaFoamDark, - RelativeSizeAxes = Axes.Both, - }, fileSelector = new OsuFileSelector(validFileExtensions: game.HandledExtensions.ToArray()) { RelativeSizeAxes = Axes.Both, @@ -72,7 +68,7 @@ namespace osu.Game.Screens.Import { new Box { - Colour = colours.GreySeaFoamDarker, + Colour = colourProvider.Background4, RelativeSizeAxes = Axes.Both }, new Container From 16fc413a4ac0a933dda126bf275b610cf1b1eef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Aug 2024 16:01:49 +0200 Subject: [PATCH 275/308] Apply NRT to directory & file selectors --- osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs | 6 ++---- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 8 +++----- .../UserInterfaceV2/OsuDirectorySelectorDirectory.cs | 4 +--- osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs | 7 +++---- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs index 4002480e7f..85599a5d45 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -20,7 +18,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 private Box hiddenToggleBackground = null!; - public OsuDirectorySelector(string initialPath = null) + public OsuDirectorySelector(string? initialPath = null) : base(initialPath) { } @@ -68,7 +66,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); - protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); + protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); protected override void NotifySelectionError() => this.FlashColour(Colour4.Red, 300); } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs index 5b52663198..e91076498c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -31,7 +29,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayComputer(); - protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName); + protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName); [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -62,13 +60,13 @@ namespace osu.Game.Graphics.UserInterfaceV2 private partial class OsuBreadcrumbDisplayDirectory : DirectorySelectorDirectory { - public OsuBreadcrumbDisplayDirectory(DirectoryInfo directory, string displayName = null) + public OsuBreadcrumbDisplayDirectory(DirectoryInfo? directory, string? displayName = null) : base(directory, displayName) { } [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs index a1d76dd7e3..a36804658a 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -15,7 +13,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { internal partial class OsuDirectorySelectorDirectory : DirectorySelectorDirectory { - public OsuDirectorySelectorDirectory(DirectoryInfo directory, string displayName = null) + public OsuDirectorySelectorDirectory(DirectoryInfo directory, string? displayName = null) : base(directory, displayName) { } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index feedeb8ff3..7ce5f63656 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.IO; using System.Linq; using osu.Framework.Allocation; @@ -22,7 +20,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 { private Box hiddenToggleBackground = null!; - public OsuFileSelector(string initialPath = null, string[] validFileExtensions = null) + public OsuFileSelector(string? initialPath = null, string[]? validFileExtensions = null) + : base(initialPath, validFileExtensions) { } @@ -69,7 +68,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory); - protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); + protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName); protected override DirectoryListingFile CreateFileItem(FileInfo file) => new OsuDirectoryListingFile(file); From 9f4e48dde78eb1b76c6131999aa21aeb4dc42843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 15:15:28 +0200 Subject: [PATCH 276/308] Actually use bindables rather than stick things in `Update()` --- osu.Game/Screens/Play/HUD/ArgonSongProgress.cs | 2 +- osu.Game/Screens/Play/HUD/DefaultSongProgress.cs | 3 +-- osu.Game/Skinning/Components/BoxElement.cs | 8 +++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index 1a18466743..92ac863e98 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -98,6 +98,7 @@ namespace osu.Game.Screens.Play.HUD Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true); + AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); } protected override void UpdateObjects(IEnumerable objects) @@ -118,7 +119,6 @@ namespace osu.Game.Screens.Play.HUD base.Update(); content.Height = bar.Height + bar_height + info.Height; graphContainer.Height = bar.Height; - Colour = AccentColour.Value; } protected override void UpdateProgress(double progress, bool isIntro) diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 93d75a22ba..4e41901ee3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -90,6 +90,7 @@ namespace osu.Game.Screens.Play.HUD Interactive.BindValueChanged(_ => updateBarVisibility(), true); ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true); ShowTime.BindValueChanged(_ => updateTimeVisibility(), true); + AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); base.LoadComplete(); } @@ -118,8 +119,6 @@ namespace osu.Game.Screens.Play.HUD if (!Precision.AlmostEquals(Height, newHeight, 5f)) content.Height = newHeight; - - Colour = AccentColour.Value; } private void updateBarVisibility() diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index 633fb0c327..7f052a8523 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -46,12 +46,18 @@ namespace osu.Game.Skinning.Components Masking = true; } + protected override void LoadComplete() + { + base.LoadComplete(); + + AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true); + } + protected override void Update() { base.Update(); base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight); - Colour = AccentColour.Value; } } } From 99a80b399cbeb1ac3a95bbec1a1ca2639f920caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 24 Sep 2024 16:42:37 +0200 Subject: [PATCH 277/308] Animate SelectionBox buttons on unfreeze --- .../Edit/Compose/Components/SelectionBox.cs | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 4eae2b77f6..d685fe74b0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -370,10 +371,14 @@ namespace osu.Game.Screens.Edit.Compose.Components private void unfreezeButtonPosition() { - frozenButtonsPosition = null; + if (frozenButtonsPosition != null) + { + frozenButtonsPosition = null; + ensureButtonsOnScreen(true); + } } - private void ensureButtonsOnScreen() + private void ensureButtonsOnScreen(bool animated = false) { if (frozenButtonsPosition != null) { @@ -384,7 +389,8 @@ namespace osu.Game.Screens.Edit.Compose.Components return; } - buttons.Position = Vector2.Zero; + if (!animated && buttons.Transforms.Any()) + return; var thisQuad = ScreenSpaceDrawQuad; @@ -399,24 +405,51 @@ namespace osu.Game.Screens.Edit.Compose.Components float minHeight = buttons.ScreenSpaceDrawQuad.Height; + Anchor targetAnchor; + Anchor targetOrigin; + Vector2 targetPosition = Vector2.Zero; + if (topExcess < minHeight && bottomExcess < minHeight) { - buttons.Anchor = Anchor.BottomCentre; - buttons.Origin = Anchor.BottomCentre; - buttons.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight); + targetAnchor = Anchor.BottomCentre; + targetOrigin = Anchor.BottomCentre; + targetPosition.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight); } else if (topExcess > bottomExcess) { - buttons.Anchor = Anchor.TopCentre; - buttons.Origin = Anchor.BottomCentre; + targetAnchor = Anchor.TopCentre; + targetOrigin = Anchor.BottomCentre; } else { - buttons.Anchor = Anchor.BottomCentre; - buttons.Origin = Anchor.TopCentre; + targetAnchor = Anchor.BottomCentre; + targetOrigin = Anchor.TopCentre; } - buttons.X += ToLocalSpace(thisQuad.TopLeft - new Vector2(Math.Min(0, leftExcess)) + new Vector2(Math.Min(0, rightExcess))).X; + targetPosition.X += ToLocalSpace(thisQuad.TopLeft - new Vector2(Math.Min(0, leftExcess)) + new Vector2(Math.Min(0, rightExcess))).X; + + if (animated) + { + var originalPosition = ToLocalSpace(buttons.ScreenSpaceDrawQuad.TopLeft); + + buttons.Origin = targetOrigin; + buttons.Anchor = targetAnchor; + buttons.Position = targetPosition; + + var newPosition = ToLocalSpace(buttons.ScreenSpaceDrawQuad.TopLeft); + + var delta = newPosition - originalPosition; + + buttons.Position -= delta; + + buttons.MoveTo(targetPosition, 300, Easing.OutQuint); + } + else + { + buttons.Anchor = targetAnchor; + buttons.Origin = targetOrigin; + buttons.Position = targetPosition; + } } } } From 555d4ffe897e9a55c674064116b51a9bd08f106d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 17:51:54 +0200 Subject: [PATCH 278/308] Add failing test case --- .../Ranking/TestSceneStatisticsPanel.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index acfa519c81..f46f76cbb8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -5,14 +5,18 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; @@ -23,6 +27,7 @@ using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Ranking.Statistics.User; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; @@ -80,6 +85,69 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(null); } + [Test] + public void TestStatisticsShownCorrectlyIfUpdateDeliveredBeforeLoad() + { + UserStatisticsWatcher userStatisticsWatcher = null!; + ScoreInfo score = null!; + + AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); + AddStep("set user statistics update", () => + { + score = TestResources.CreateTestScoreInfo(); + score.OnlineID = 1234; + ((Bindable)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score, + new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 20, + }, + GlobalRank = 38000, + CountryRank = 12006, + PP = 2134, + RankedScore = 21123849, + Accuracy = 0.985, + PlayCount = 13375, + PlayTime = 354490, + TotalScore = 128749597, + TotalHits = 0, + MaxCombo = 1233, + }, new UserStatistics + { + Level = new UserStatistics.LevelInfo + { + Current = 5, + Progress = 30, + }, + GlobalRank = 36000, + CountryRank = 12000, + PP = (decimal)2134.5, + RankedScore = 23897015, + Accuracy = 0.984, + PlayCount = 13376, + PlayTime = 35789, + TotalScore = 132218497, + TotalHits = 0, + MaxCombo = 1233, + }); + }); + AddStep("load user statistics panel", () => Child = new DependencyProvidingContainer + { + CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)], + RelativeSizeAxes = Axes.Both, + Child = new UserStatisticsPanel(score) + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Score = { Value = score, } + } + }); + AddUntilStep("overall ranking present", () => this.ChildrenOfType().Any()); + AddUntilStep("loading spinner not visible", () => this.ChildrenOfType().All(l => l.State.Value == Visibility.Hidden)); + } + private void loadPanel(ScoreInfo score) => AddStep("load panel", () => { Child = new UserStatisticsPanel(score) From 20e7ade3b0a48f670da7f6f1d4ddedda1f2ddc44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 24 Sep 2024 17:52:19 +0200 Subject: [PATCH 279/308] Fix statistics update not being shown on results screen if it arrives too fast As reported in https://discord.com/channels/188630481301012481/1097318920991559880/1288160137286258799. --- osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs index fa3bb1a375..4e9c07ab7b 100644 --- a/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/UserStatisticsPanel.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking.Statistics { if (update.NewValue?.Score.MatchesOnlineID(achievedScore) == true) DisplayedUserStatisticsUpdate.Value = update.NewValue; - }); + }, true); } } From 2d95c0b0bbaf6d97c940414f2e6afc61aa15e656 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 24 Sep 2024 18:45:52 +0200 Subject: [PATCH 280/308] remove tail recursion form welzl --- osu.Game/Utils/GeometryUtils.cs | 54 ++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index d4968749bf..877f58769b 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -305,33 +305,37 @@ namespace osu.Game.Utils // n represents the number of points in P that are not yet processed. private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) { - // Base case when all points processed or |R| = 3 - if (n == 0 || r.Length == 3) - return minCircleTrivial(r); - - // Pick a random point randomly - int idx = RNG.Next(n); - Vector2 p = points[idx]; - - // Put the picked point at the end of P since it's more efficient than - // deleting from the middle of the list - (points[idx], points[n - 1]) = (points[n - 1], points[idx]); - - // Get the MEC circle d from the set of points P - {p} - var d = welzlHelper(points, r, n - 1); - - // If d contains p, return d - if (isInside(d, p)) - return d; - - // Otherwise, must be on the boundary of the MEC - // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 - Span r2 = stackalloc Vector2[r.Length + 1]; + Span r2 = stackalloc Vector2[3]; + int rLength = r.Length; r.CopyTo(r2); - r2[r.Length] = p; - // Return the MEC for P - {p} and R U {p} - return welzlHelper(points, r2, n - 1); + while (true) + { + // Base case when all points processed or |R| = 3 + if (n == 0 || rLength == 3) return minCircleTrivial(r2[..rLength]); + + // Pick a random point randomly + int idx = RNG.Next(n); + Vector2 p = points[idx]; + + // Put the picked point at the end of P since it's more efficient than + // deleting from the middle of the list + (points[idx], points[n - 1]) = (points[n - 1], points[idx]); + + // Get the MEC circle d from the set of points P - {p} + var d = welzlHelper(points, r2[..rLength], n - 1); + + // If d contains p, return d + if (isInside(d, p)) return d; + + // Otherwise, must be on the boundary of the MEC + // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 + r2[rLength] = p; + rLength++; + + // Return the MEC for P - {p} and R U {p} + n--; + } } #endregion From 796fc948e138f839b083826fb69e6130e303c0c2 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Tue, 24 Sep 2024 20:15:03 +0200 Subject: [PATCH 281/308] Rewrite Welzl's algorithm to use no recursion --- osu.Game/Utils/GeometryUtils.cs | 104 ++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index 877f58769b..e9e79deb49 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -255,7 +255,7 @@ namespace osu.Game.Utils } // Function to check whether a circle encloses the given points - private static bool isValidCircle((Vector2, float) c, ReadOnlySpan points) + private static bool isValidCircle((Vector2, float) c, List points) { // Iterating through all the points to check whether the points lie inside the circle or not foreach (Vector2 p in points) @@ -267,12 +267,12 @@ namespace osu.Game.Utils } // Function to return the minimum enclosing circle for N <= 3 - private static (Vector2, float) minCircleTrivial(ReadOnlySpan points) + private static (Vector2, float) minCircleTrivial(List points) { - if (points.Length > 3) + if (points.Count > 3) throw new ArgumentException("Number of points must be at most 3", nameof(points)); - switch (points.Length) + switch (points.Count) { case 0: return (new Vector2(0, 0), 0); @@ -299,45 +299,6 @@ namespace osu.Game.Utils return circleFrom(points[0], points[1], points[2]); } - // Returns the MEC using Welzl's algorithm - // Takes a set of input points P and a set R - // points on the circle boundary. - // n represents the number of points in P that are not yet processed. - private static (Vector2, float) welzlHelper(List points, ReadOnlySpan r, int n) - { - Span r2 = stackalloc Vector2[3]; - int rLength = r.Length; - r.CopyTo(r2); - - while (true) - { - // Base case when all points processed or |R| = 3 - if (n == 0 || rLength == 3) return minCircleTrivial(r2[..rLength]); - - // Pick a random point randomly - int idx = RNG.Next(n); - Vector2 p = points[idx]; - - // Put the picked point at the end of P since it's more efficient than - // deleting from the middle of the list - (points[idx], points[n - 1]) = (points[n - 1], points[idx]); - - // Get the MEC circle d from the set of points P - {p} - var d = welzlHelper(points, r2[..rLength], n - 1); - - // If d contains p, return d - if (isInside(d, p)) return d; - - // Otherwise, must be on the boundary of the MEC - // Stackalloc to avoid allocations. It's safe to assume that the length of r will be at most 3 - r2[rLength] = p; - rLength++; - - // Return the MEC for P - {p} and R U {p} - n--; - } - } - #endregion /// @@ -348,8 +309,61 @@ namespace osu.Game.Utils { // Using Welzl's algorithm to find the minimum enclosing circle // https://www.geeksforgeeks.org/minimum-enclosing-circle-using-welzls-algorithm/ - List pCopy = points.ToList(); - return welzlHelper(pCopy, Array.Empty(), pCopy.Count); + List P = points.ToList(); + + var stack = new Stack<(Vector2?, int)>(); + var r = new List(3); + (Vector2, float) d = (Vector2.Zero, 0); + + stack.Push((null, P.Count)); + + while (stack.Count > 0) + { + // n represents the number of points in P that are not yet processed. + // p represents the point that was randomly picked to process. + (Vector2? p, int n) = stack.Pop(); + + if (!p.HasValue) + { + // Base case when all points processed or |R| = 3 + if (n == 0 || r.Count == 3) + { + d = minCircleTrivial(r); + continue; + } + + // Pick a random point randomly + int idx = RNG.Next(n); + p = P[idx]; + + // Put the picked point at the end of P since it's more efficient than + // deleting from the middle of the list + (P[idx], P[n - 1]) = (P[n - 1], P[idx]); + + // Schedule processing of p after we get the MEC circle d from the set of points P - {p} + stack.Push((p, n)); + // Get the MEC circle d from the set of points P - {p} + stack.Push((null, n - 1)); + } + else + { + // If d contains p, return d + if (isInside(d, p.Value)) + continue; + + // Remove points from R that were added in a deeper recursion + // |R| = |P| - |stack| - n + int removeCount = r.Count - (P.Count - stack.Count - n); + r.RemoveRange(r.Count - removeCount, removeCount); + + // Otherwise, must be on the boundary of the MEC + r.Add(p.Value); + // Return the MEC for P - {p} and R U {p} + stack.Push((null, n - 1)); + } + } + + return d; } /// From e3b4483872ab71fcc1e700dd1195294f650e5c1b Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 16:33:36 +0200 Subject: [PATCH 282/308] Refactor PlacementBlueprint to not be hitobject specific --- .../CatchPlacementBlueprintTestScene.cs | 2 +- ...TestSceneBananaShowerPlacementBlueprint.cs | 4 +- .../TestSceneFruitPlacementBlueprint.cs | 2 +- .../TestSceneJuiceStreamPlacementBlueprint.cs | 2 +- .../Edit/BananaShowerCompositionTool.cs | 4 +- .../Blueprints/CatchPlacementBlueprint.cs | 2 +- .../Edit/CatchHitObjectComposer.cs | 4 +- .../Edit/FruitCompositionTool.cs | 4 +- .../Edit/JuiceStreamCompositionTool.cs | 4 +- .../ManiaPlacementBlueprintTestScene.cs | 2 +- .../TestSceneHoldNotePlacementBlueprint.cs | 2 +- .../Editor/TestSceneNotePlacementBlueprint.cs | 2 +- .../Blueprints/ManiaPlacementBlueprint.cs | 2 +- .../Edit/HoldNoteCompositionTool.cs | 4 +- .../Edit/ManiaHitObjectComposer.cs | 2 +- .../Edit/NoteCompositionTool.cs | 4 +- .../TestSceneHitCirclePlacementBlueprint.cs | 2 +- .../TestSceneSliderPlacementBlueprint.cs | 2 +- .../TestSceneSpinnerPlacementBlueprint.cs | 2 +- .../HitCircles/HitCirclePlacementBlueprint.cs | 2 +- .../Sliders/SliderPlacementBlueprint.cs | 2 +- .../Spinners/SpinnerPlacementBlueprint.cs | 2 +- .../Edit/HitCircleCompositionTool.cs | 4 +- .../Edit/OsuHitObjectComposer.cs | 2 +- .../Edit/SliderCompositionTool.cs | 4 +- .../Edit/SpinnerCompositionTool.cs | 4 +- .../Edit/Blueprints/HitPlacementBlueprint.cs | 2 +- .../Blueprints/TaikoSpanPlacementBlueprint.cs | 2 +- .../Edit/DrumRollCompositionTool.cs | 4 +- .../Edit/HitCompositionTool.cs | 4 +- .../Edit/SwellCompositionTool.cs | 4 +- .../Edit/TaikoHitObjectComposer.cs | 2 +- .../Editing/TestScenePlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 4 +- .../Edit/HitObjectCompositionToolButton.cs | 4 +- .../Edit/HitObjectPlacementBlueprint.cs | 126 ++++++++++++++++++ osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 122 +++-------------- ...tCompositionTool.cs => CompositionTool.cs} | 4 +- osu.Game/Rulesets/Edit/Tools/SelectTool.cs | 4 +- .../Components/ComposeBlueprintContainer.cs | 22 +-- .../Visual/PlacementBlueprintTestScene.cs | 8 +- 41 files changed, 212 insertions(+), 174 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs rename osu.Game/Rulesets/Edit/Tools/{HitObjectCompositionTool.cs => CompositionTool.cs} (84%) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs index aae759d934..0578010c25 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchPlacementBlueprintTestScene.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor contentContainer.Playfield.HitObjectContainer.Add(hitObject); } - protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint) + protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) { var result = base.SnapForBlueprint(blueprint); result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index ed37ff4ef3..badd8e967d 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint(); protected override void AddHitObject(DrawableHitObject hitObject) { @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddClickStep(MouseButton.Left); AddClickStep(MouseButton.Right); AddAssert("banana shower is not placed", () => LastObject == null); - AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting); + AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == HitObjectPlacementBlueprint.PlacementState.Waiting); } [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs index 75d3c3753a..80cd948e26 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneFruitPlacementBlueprint.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint(); [Test] public void TestFruitPlacementPosition() diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs index d010bb02ad..8bd60078c6 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableJuiceStream((JuiceStream)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new JuiceStreamPlacementBlueprint(); private void addMoveAndClickSteps(double time, float position, bool end = false) { diff --git a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs index 31075db7d1..be93ba0242 100644 --- a/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs +++ b/osu.Game.Rulesets.Catch/Edit/BananaShowerCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools; namespace osu.Game.Rulesets.Catch.Edit { - public class BananaShowerCompositionTool : HitObjectCompositionTool + public class BananaShowerCompositionTool : CompositionTool { public BananaShowerCompositionTool() : base(nameof(BananaShower)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); - public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs index 1a2990e4ac..aa862375c5 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/CatchPlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Catch.Edit.Blueprints { - public partial class CatchPlacementBlueprint : PlacementBlueprint + public partial class CatchPlacementBlueprint : HitObjectPlacementBlueprint where THitObject : CatchHitObject, new() { protected new THitObject HitObject => (THitObject)base.HitObject; diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 83f48816f9..8460e238f6 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Edit protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid(); - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + protected override IReadOnlyList CompositionTools => new CompositionTool[] { new FruitCompositionTool(), new JuiceStreamCompositionTool(), @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Catch.Edit if (EditorBeatmap.PlacementObject.Value is JuiceStream) { // Juice stream path is not subject to snapping. - if (BlueprintContainer.CurrentPlacement.PlacementActive is PlacementBlueprint.PlacementState.Active) + if (BlueprintContainer.CurrentPlacement.PlacementActive is HitObjectPlacementBlueprint.PlacementState.Active) return null; } diff --git a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs index f776fe39c1..71c1e66903 100644 --- a/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs +++ b/osu.Game.Rulesets.Catch/Edit/FruitCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools; namespace osu.Game.Rulesets.Catch.Edit { - public class FruitCompositionTool : HitObjectCompositionTool + public class FruitCompositionTool : CompositionTool { public FruitCompositionTool() : base(nameof(Fruit)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); - public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs b/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs index cb66e2952e..7a567820f3 100644 --- a/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs +++ b/osu.Game.Rulesets.Catch/Edit/JuiceStreamCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit.Tools; namespace osu.Game.Rulesets.Catch.Edit { - public class JuiceStreamCompositionTool : HitObjectCompositionTool + public class JuiceStreamCompositionTool : CompositionTool { public JuiceStreamCompositionTool() : base(nameof(JuiceStream)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); - public override PlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new JuiceStreamPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index d2a44122aa..5e633c3161 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor }); } - protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint) + protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) { double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); var pos = column.ScreenSpacePositionAtTime(time); diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs index b79bcb7682..2006879ab3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs @@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor public partial class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHoldNote((HoldNote)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new HoldNotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs index a446f13cbf..0cb9639cd1 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs @@ -64,6 +64,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor private Note getNote() => this.ChildrenOfType().FirstOrDefault()?.HitObject; protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableNote((Note)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new NotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 5e0512b5dc..a68bd5d6d6 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -15,7 +15,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { - public abstract partial class ManiaPlacementBlueprint : PlacementBlueprint + public abstract partial class ManiaPlacementBlueprint : HitObjectPlacementBlueprint where T : ManiaHitObject { protected new T HitObject => (T)base.HitObject; diff --git a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs index 99e1ce04b1..592f8d9af7 100644 --- a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mania.Edit.Blueprints; namespace osu.Game.Rulesets.Mania.Edit { - public class HoldNoteCompositionTool : HitObjectCompositionTool + public class HoldNoteCompositionTool : CompositionTool { public HoldNoteCompositionTool() : base("Hold") @@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); - public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 02a4f3a022..e3b4fa2fb7 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Edit protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid(); - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + protected override IReadOnlyList CompositionTools => new CompositionTool[] { new NoteCompositionTool(), new HoldNoteCompositionTool() diff --git a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs index 08ee05ad3f..2e54d63525 100644 --- a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mania.Objects; namespace osu.Game.Rulesets.Mania.Edit { - public class NoteCompositionTool : HitObjectCompositionTool + public class NoteCompositionTool : CompositionTool { public NoteCompositionTool() : base(nameof(Note)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Mania.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); - public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index a49afd82f3..a105d860bf 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index aa6a6f08d8..019565ae29 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -514,6 +514,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private Slider? getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null; protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index 0e8673319e..d7b5cc73be 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject); - protected override PlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint(); + protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 20ad99baa2..78a0e36dc2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -9,7 +9,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles { - public partial class HitCirclePlacementBlueprint : PlacementBlueprint + public partial class HitCirclePlacementBlueprint : HitObjectPlacementBlueprint { public new HitCircle HitObject => (HitCircle)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 42945295b8..6ffe27dc13 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -21,7 +21,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { - public partial class SliderPlacementBlueprint : PlacementBlueprint + public partial class SliderPlacementBlueprint : HitObjectPlacementBlueprint { public new Slider HitObject => (Slider)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index f59be0e0e9..17d2dcd75c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -13,7 +13,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners { - public partial class SpinnerPlacementBlueprint : PlacementBlueprint + public partial class SpinnerPlacementBlueprint : HitObjectPlacementBlueprint { public new Spinner HitObject => (Spinner)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs index c41ae10b2e..d3116ede30 100644 --- a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Edit { - public class HitCircleCompositionTool : HitObjectCompositionTool + public class HitCircleCompositionTool : CompositionTool { public HitCircleCompositionTool() : base(nameof(HitCircle)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); - public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 8fc2a9b7d3..4368a338b2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) => new DrawableOsuEditorRuleset(ruleset, beatmap, mods); - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + protected override IReadOnlyList CompositionTools => new CompositionTool[] { new HitCircleCompositionTool(), new SliderCompositionTool(), diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs index 617cc1c19b..d697a2ebe6 100644 --- a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Edit { - public class SliderCompositionTool : HitObjectCompositionTool + public class SliderCompositionTool : CompositionTool { public SliderCompositionTool() : base(nameof(Slider)) @@ -26,6 +26,6 @@ namespace osu.Game.Rulesets.Osu.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); - public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs index c8160617c9..de1506e4a9 100644 --- a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Edit { - public class SpinnerCompositionTool : HitObjectCompositionTool + public class SpinnerCompositionTool : CompositionTool { public SpinnerCompositionTool() : base(nameof(Spinner)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); - public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index 329fff5b42..7f45123bd6 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -10,7 +10,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { - public partial class HitPlacementBlueprint : PlacementBlueprint + public partial class HitPlacementBlueprint : HitObjectPlacementBlueprint { private readonly HitPiece piece; diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index cd52398086..de3a4d96eb 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -17,7 +17,7 @@ using osuTK.Input; namespace osu.Game.Rulesets.Taiko.Edit.Blueprints { - public partial class TaikoSpanPlacementBlueprint : PlacementBlueprint + public partial class TaikoSpanPlacementBlueprint : HitObjectPlacementBlueprint { private readonly HitPiece headPiece; private readonly HitPiece tailPiece; diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs index f332441875..ba0fda6771 100644 --- a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit { - public class DrumRollCompositionTool : HitObjectCompositionTool + public class DrumRollCompositionTool : CompositionTool { public DrumRollCompositionTool() : base(nameof(DrumRoll)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); - public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs index fa50841893..f58defba83 100644 --- a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit { - public class HitCompositionTool : HitObjectCompositionTool + public class HitCompositionTool : CompositionTool { public HitCompositionTool() : base(nameof(Hit)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); - public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs index 4d4ee8effe..4ec623e29e 100644 --- a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Edit { - public class SwellCompositionTool : HitObjectCompositionTool + public class SwellCompositionTool : CompositionTool { public SwellCompositionTool() : base(nameof(Swell)) @@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Edit public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); - public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index 6020f6e04c..d97a854ff7 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.Edit { } - protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] + protected override IReadOnlyList CompositionTools => new CompositionTool[] { new HitCompositionTool(), new DrumRollCompositionTool(), diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index fe74e1b346..6f3342f8ce 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("editor is still current", () => Editor.IsCurrentScreen()); AddAssert("slider not placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(0)); AddAssert("no active placement", () => this.ChildrenOfType().Single().CurrentPlacement.PlacementActive, - () => Is.EqualTo(PlacementBlueprint.PlacementState.Waiting)); + () => Is.EqualTo(HitObjectPlacementBlueprint.PlacementState.Waiting)); } [Test] diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c2a7bec9f9..00de46b726 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.Edit /// /// A "select" tool is automatically added as the first tool. /// - protected abstract IReadOnlyList CompositionTools { get; } + protected abstract IReadOnlyList CompositionTools { get; } /// /// A collection of states which will be displayed to the user in the toolbox. @@ -466,7 +466,7 @@ namespace osu.Game.Rulesets.Edit private void setSelectTool() => toolboxCollection.Items.First().Select(); - private void toolSelected(HitObjectCompositionTool tool) + private void toolSelected(CompositionTool tool) { BlueprintContainer.CurrentTool = tool; diff --git a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs index ba566ff5c0..641d60dbd3 100644 --- a/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs +++ b/osu.Game/Rulesets/Edit/HitObjectCompositionToolButton.cs @@ -9,9 +9,9 @@ namespace osu.Game.Rulesets.Edit { public class HitObjectCompositionToolButton : RadioButton { - public HitObjectCompositionTool Tool { get; } + public CompositionTool Tool { get; } - public HitObjectCompositionToolButton(HitObjectCompositionTool tool, Action? action) + public HitObjectCompositionToolButton(CompositionTool tool, Action? action) : base(tool.Name, action, tool.CreateIcon) { Tool = tool; diff --git a/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs new file mode 100644 index 0000000000..74025b4260 --- /dev/null +++ b/osu.Game/Rulesets/Edit/HitObjectPlacementBlueprint.cs @@ -0,0 +1,126 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose; + +namespace osu.Game.Rulesets.Edit +{ + /// + /// A blueprint which governs the creation of a new to actualisation. + /// + public abstract partial class HitObjectPlacementBlueprint : PlacementBlueprint + { + /// + /// Whether the sample bank should be taken from the previous hit object. + /// + public bool AutomaticBankAssignment { get; set; } + + /// + /// The that is being placed. + /// + public readonly HitObject HitObject; + + [Resolved] + protected EditorClock EditorClock { get; private set; } = null!; + + [Resolved] + private EditorBeatmap beatmap { get; set; } = null!; + + private Bindable startTimeBindable = null!; + + private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault(); + + [Resolved] + private IPlacementHandler placementHandler { get; set; } = null!; + + protected HitObjectPlacementBlueprint(HitObject hitObject) + { + HitObject = hitObject; + + // adding the default hit sample should be the case regardless of the ruleset. + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); + } + + [BackgroundDependencyLoader] + private void load() + { + startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); + startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); + } + + protected override void BeginPlacement(bool commitStart = false) + { + base.BeginPlacement(commitStart); + + placementHandler.BeginPlacement(HitObject); + } + + public override void EndPlacement(bool commit) + { + base.EndPlacement(commit); + + placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit); + } + + /// + /// Updates the time and position of this based on the provided snap information. + /// + /// The snap result information. + public override void UpdateTimeAndPosition(SnapResult result) + { + if (PlacementActive == PlacementState.Waiting) + { + HitObject.StartTime = result.Time ?? EditorClock.CurrentTime; + + if (HitObject is IHasComboInformation comboInformation) + comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); + } + + var lastHitObject = getPreviousHitObject(); + + if (AutomaticBankAssignment) + { + // Create samples based on the sample settings of the previous hit object + if (lastHitObject != null) + { + for (int i = 0; i < HitObject.Samples.Count; i++) + HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name); + } + } + else + { + var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); + + if (lastHitNormal != null) + { + // Only inherit the volume from the previous hit object + for (int i = 0; i < HitObject.Samples.Count; i++) + HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume); + } + } + + if (HitObject is IHasRepeats hasRepeats) + { + // Make sure all the node samples are identical to the hit object's samples + for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) + hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); + } + } + + /// + /// Invokes , + /// refreshing and parameters for the . + /// + protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); + } +} diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index a50a7f4169..d2a54e8e03 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -1,29 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; -using System.Threading; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Audio; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Compose; using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Edit { /// - /// A blueprint which governs the creation of a new to actualisation. + /// A blueprint which governs the placement of something. /// public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler { @@ -32,29 +22,6 @@ namespace osu.Game.Rulesets.Edit /// public PlacementState PlacementActive { get; private set; } - /// - /// Whether the sample bank should be taken from the previous hit object. - /// - public bool AutomaticBankAssignment { get; set; } - - /// - /// The that is being placed. - /// - public readonly HitObject HitObject; - - [Resolved] - protected EditorClock EditorClock { get; private set; } = null!; - - [Resolved] - private EditorBeatmap beatmap { get; set; } = null!; - - private Bindable startTimeBindable = null!; - - private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault(); - - [Resolved] - private IPlacementHandler placementHandler { get; set; } = null!; - /// /// Whether this blueprint is currently in a state that can be committed. /// @@ -64,13 +31,8 @@ namespace osu.Game.Rulesets.Edit /// protected virtual bool IsValidForPlacement => true; - protected PlacementBlueprint(HitObject hitObject) + protected PlacementBlueprint() { - HitObject = hitObject; - - // adding the default hit sample should be the case regardless of the ruleset. - HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); - RelativeSizeAxes = Axes.Both; // This is required to allow the blueprint's position to be updated via OnMouseMove/Handle @@ -78,30 +40,22 @@ namespace osu.Game.Rulesets.Edit AlwaysPresent = true; } - [BackgroundDependencyLoader] - private void load() - { - startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); - startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); - } - /// - /// Signals that the placement of has started. + /// Signals that the placement has started. /// - /// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments. - protected void BeginPlacement(bool commitStart = false) + /// Whether this call is committing a value and continuing with further adjustments. + protected virtual void BeginPlacement(bool commitStart = false) { - placementHandler.BeginPlacement(HitObject); if (commitStart) PlacementActive = PlacementState.Active; } /// /// Signals that the placement of has finished. - /// This will destroy this , and add the HitObject.StartTime to the . + /// This will destroy this , and commit the changes. /// - /// Whether the object should be committed. Note that a commit may fail if is false. - public void EndPlacement(bool commit) + /// Whether the changes should be committed. Note that a commit may fail if is false. + public virtual void EndPlacement(bool commit) { switch (PlacementActive) { @@ -114,10 +68,17 @@ namespace osu.Game.Rulesets.Edit break; } - placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit); PlacementActive = PlacementState.Finished; } + /// + /// Updates the time and position of this based on the provided snap information. + /// + /// The snap result information. + public virtual void UpdateTimeAndPosition(SnapResult result) + { + } + public bool OnPressed(KeyBindingPressEvent e) { if (PlacementActive == PlacementState.Waiting) @@ -138,57 +99,6 @@ namespace osu.Game.Rulesets.Edit { } - /// - /// Updates the time and position of this based on the provided snap information. - /// - /// The snap result information. - public virtual void UpdateTimeAndPosition(SnapResult result) - { - if (PlacementActive == PlacementState.Waiting) - { - HitObject.StartTime = result.Time ?? EditorClock.CurrentTime; - - if (HitObject is IHasComboInformation comboInformation) - comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation); - } - - var lastHitObject = getPreviousHitObject(); - - if (AutomaticBankAssignment) - { - // Create samples based on the sample settings of the previous hit object - if (lastHitObject != null) - { - for (int i = 0; i < HitObject.Samples.Count; i++) - HitObject.Samples[i] = lastHitObject.CreateHitSampleInfo(HitObject.Samples[i].Name); - } - } - else - { - var lastHitNormal = lastHitObject?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL); - - if (lastHitNormal != null) - { - // Only inherit the volume from the previous hit object - for (int i = 0; i < HitObject.Samples.Count; i++) - HitObject.Samples[i] = HitObject.Samples[i].With(newVolume: lastHitNormal.Volume); - } - } - - if (HitObject is IHasRepeats hasRepeats) - { - // Make sure all the node samples are identical to the hit object's samples - for (int i = 0; i < hasRepeats.NodeSamples.Count; i++) - hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList(); - } - } - - /// - /// Invokes , - /// refreshing and parameters for the . - /// - protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; protected override bool Handle(UIEvent e) diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/CompositionTool.cs similarity index 84% rename from osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs rename to osu.Game/Rulesets/Edit/Tools/CompositionTool.cs index ba1dc817bb..f509302daa 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/CompositionTool.cs @@ -6,13 +6,13 @@ using osu.Framework.Localisation; namespace osu.Game.Rulesets.Edit.Tools { - public abstract class HitObjectCompositionTool + public abstract class CompositionTool { public readonly string Name; public LocalisableString TooltipText { get; init; } - protected HitObjectCompositionTool(string name) + protected CompositionTool(string name) { Name = name; } diff --git a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs index a272e9f480..7f8889bfca 100644 --- a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics; namespace osu.Game.Rulesets.Edit.Tools { - public class SelectTool : HitObjectCompositionTool + public class SelectTool : CompositionTool { public SelectTool() : base("Select") @@ -18,6 +18,6 @@ namespace osu.Game.Rulesets.Edit.Tools public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.EditorSelect }; - public override PlacementBlueprint CreatePlacementBlueprint() => null; + public override HitObjectPlacementBlueprint CreatePlacementBlueprint() => null; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index f1294ccc3c..f0296d45aa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -41,6 +41,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public PlacementBlueprint CurrentPlacement { get; private set; } + public HitObjectPlacementBlueprint CurrentHitObjectPlacement => CurrentPlacement as HitObjectPlacementBlueprint; + [Resolved(canBeNull: true)] private EditorScreenWithTimeline editorScreen { get; set; } @@ -164,13 +166,13 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementNewCombo() { - if (CurrentPlacement?.HitObject is IHasComboInformation c) + if (CurrentHitObjectPlacement?.HitObject is IHasComboInformation c) c.NewCombo = NewCombo.Value == TernaryState.True; } private void updatePlacementSamples() { - if (CurrentPlacement == null) return; + if (CurrentHitObjectPlacement == null) return; foreach (var kvp in SelectionHandler.SelectionSampleStates) sampleChanged(kvp.Key, kvp.Value.Value); @@ -181,9 +183,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private void sampleChanged(string sampleName, TernaryState state) { - if (CurrentPlacement == null) return; + if (CurrentHitObjectPlacement == null) return; - var samples = CurrentPlacement.HitObject.Samples; + var samples = CurrentHitObjectPlacement.HitObject.Samples; var existingSample = samples.FirstOrDefault(s => s.Name == sampleName); @@ -196,19 +198,19 @@ namespace osu.Game.Screens.Edit.Compose.Components case TernaryState.True: if (existingSample == null) - samples.Add(CurrentPlacement.HitObject.CreateHitSampleInfo(sampleName)); + samples.Add(CurrentHitObjectPlacement.HitObject.CreateHitSampleInfo(sampleName)); break; } } private void bankChanged(string bankName, TernaryState state) { - if (CurrentPlacement == null) return; + if (CurrentHitObjectPlacement == null) return; if (bankName == EditorSelectionHandler.HIT_BANK_AUTO) - CurrentPlacement.AutomaticBankAssignment = state == TernaryState.True; + CurrentHitObjectPlacement.AutomaticBankAssignment = state == TernaryState.True; else if (state == TernaryState.True) - CurrentPlacement.HitObject.Samples = CurrentPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); + CurrentHitObjectPlacement.HitObject.Samples = CurrentHitObjectPlacement.HitObject.Samples.Select(s => s.With(newBank: bankName)).ToList(); } public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; @@ -386,12 +388,12 @@ namespace osu.Game.Screens.Edit.Compose.Components CurrentPlacement = null; } - private HitObjectCompositionTool currentTool; + private CompositionTool currentTool; /// /// The current placement tool. /// - public HitObjectCompositionTool CurrentTool + public CompositionTool CurrentTool { get => currentTool; diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index 0027e03492..c8d9ef8fc8 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual public abstract partial class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler { protected readonly Container HitObjectContainer; - protected PlacementBlueprint CurrentBlueprint { get; private set; } + protected HitObjectPlacementBlueprint CurrentBlueprint { get; private set; } protected PlacementBlueprintTestScene() { @@ -87,14 +87,14 @@ namespace osu.Game.Tests.Visual CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint)); } - protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) => + protected virtual SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) => new SnapResult(InputManager.CurrentState.Mouse.Position, null); public override void Add(Drawable drawable) { base.Add(drawable); - if (drawable is PlacementBlueprint blueprint) + if (drawable is HitObjectPlacementBlueprint blueprint) { blueprint.Show(); blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); @@ -106,6 +106,6 @@ namespace osu.Game.Tests.Visual protected virtual Container CreateHitObjectContainer() => new Container { RelativeSizeAxes = Axes.Both }; protected abstract DrawableHitObject CreateHitObject(HitObject hitObject); - protected abstract PlacementBlueprint CreateBlueprint(); + protected abstract HitObjectPlacementBlueprint CreateBlueprint(); } } From d26e677bb7ceb9e35e7af22c77c9ccfe39672483 Mon Sep 17 00:00:00 2001 From: OliBomby Date: Mon, 23 Sep 2024 17:34:45 +0200 Subject: [PATCH 283/308] fix warnings --- .../Editor/TestSceneBananaShowerPlacementBlueprint.cs | 2 +- osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs | 2 +- osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index badd8e967d..296d34d628 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddClickStep(MouseButton.Left); AddClickStep(MouseButton.Right); AddAssert("banana shower is not placed", () => LastObject == null); - AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == HitObjectPlacementBlueprint.PlacementState.Waiting); + AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting); } [Test] diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 8460e238f6..7323c7a91a 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Catch.Edit if (EditorBeatmap.PlacementObject.Value is JuiceStream) { // Juice stream path is not subject to snapping. - if (BlueprintContainer.CurrentPlacement.PlacementActive is HitObjectPlacementBlueprint.PlacementState.Active) + if (BlueprintContainer.CurrentPlacement.PlacementActive is PlacementBlueprint.PlacementState.Active) return null; } diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs index 6f3342f8ce..fe74e1b346 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("editor is still current", () => Editor.IsCurrentScreen()); AddAssert("slider not placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(0)); AddAssert("no active placement", () => this.ChildrenOfType().Single().CurrentPlacement.PlacementActive, - () => Is.EqualTo(HitObjectPlacementBlueprint.PlacementState.Waiting)); + () => Is.EqualTo(PlacementBlueprint.PlacementState.Waiting)); } [Test] From 3ab04d98f618df16362e5caf25c908df07012226 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 16:45:37 +0900 Subject: [PATCH 284/308] Fix Realm-related iOS crashes by removing object references --- osu.Game/Beatmaps/BeatmapImporter.cs | 4 +++- osu.Game/Online/BeatmapDownloadTracker.cs | 4 +++- osu.Game/Online/ScoreDownloadTracker.cs | 10 +++++++--- osu.Game/Skinning/RealmBackedResourceStore.cs | 4 +++- osu.Game/Skinning/SkinManager.cs | 4 +++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 8acaebd1a8..63d8215d73 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -198,8 +198,10 @@ namespace osu.Game.Beatmaps if (beatmapSet.OnlineID > 0) { + int onlineId = beatmapSet.OnlineID; + // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure. - foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == beatmapSet.OnlineID)) + foreach (var existingSetWithSameOnlineID in realm.All().Where(b => b.OnlineID == onlineId)) { existingSetWithSameOnlineID.DeletePending = true; existingSetWithSameOnlineID.OnlineID = -1; diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index 3db602c353..c1c3d17ff6 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,7 +40,9 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; - realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _) => + int onlineId = TrackedItem.OnlineID; + + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == onlineId && !s.DeletePending), (items, _) => { if (items.Any()) Schedule(() => UpdateState(DownloadState.LocallyAvailable)); diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index dfdac24d19..eb687a7023 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -46,10 +46,14 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; + long onlineId = TrackedItem.OnlineID; + long legacyOnlineId = TrackedItem.LegacyOnlineID; + string hash = TrackedItem.Hash; + realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => - ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) - || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID) - || (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) + ((s.OnlineID > 0 && s.OnlineID == onlineId) + || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == legacyOnlineId) + || (!string.IsNullOrEmpty(s.Hash) && s.Hash == hash)) && !s.DeletePending), (items, _) => { if (items.Any()) diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index cce099a268..b1289bd0c5 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -29,7 +29,9 @@ namespace osu.Game.Skinning invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); - realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == source.ID), skinChanged); + Guid id = source.ID; + + realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == id), skinChanged); } protected override void Dispose(bool disposing) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 4f816d88d2..cd431bd80c 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -131,9 +131,11 @@ namespace osu.Game.Skinning { Realm.Run(r => { + Guid currentSkinId = CurrentSkinInfo.Value.ID; + // choose from only user skins, removing the current selection to ensure a new one is chosen. var randomChoices = r.All() - .Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID) + .Where(s => !s.DeletePending && s.ID != currentSkinId) .ToArray(); if (randomChoices.Length == 0) From fd4891cf31de0f5e75b937fdf168a17c16e65a3b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 20:34:50 +0900 Subject: [PATCH 285/308] Fix similar Bindable-related crashes --- .../Utils/BindableValueAccessorTest.cs | 52 +++++++++++++++++++ .../Configuration/SettingSourceAttribute.cs | 7 +-- osu.Game/Rulesets/Mods/Mod.cs | 3 +- osu.Game/Utils/BindableValueAccessor.cs | 44 ++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Tests/Utils/BindableValueAccessorTest.cs create mode 100644 osu.Game/Utils/BindableValueAccessor.cs diff --git a/osu.Game.Tests/Utils/BindableValueAccessorTest.cs b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs new file mode 100644 index 0000000000..f09623dbfc --- /dev/null +++ b/osu.Game.Tests/Utils/BindableValueAccessorTest.cs @@ -0,0 +1,52 @@ +// 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.Bindables; +using osu.Game.Utils; + +namespace osu.Game.Tests.Utils +{ + [TestFixture] + public class BindableValueAccessorTest + { + [Test] + public void GetValue() + { + const int value = 1337; + + BindableInt bindable = new BindableInt(value); + Assert.That(BindableValueAccessor.GetValue(bindable), Is.EqualTo(value)); + } + + [Test] + public void SetValue() + { + const int value = 1337; + + BindableInt bindable = new BindableInt(); + BindableValueAccessor.SetValue(bindable, value); + + Assert.That(bindable.Value, Is.EqualTo(value)); + } + + [Test] + public void GetInvalidBindable() + { + BindableList list = new BindableList(); + Assert.That(BindableValueAccessor.GetValue(list), Is.EqualTo(list)); + } + + [Test] + public void SetInvalidBindable() + { + const int value = 1337; + + BindableList list = new BindableList { value }; + BindableValueAccessor.SetValue(list, 2); + + Assert.That(list, Has.Exactly(1).Items); + Assert.That(list[0], Is.EqualTo(value)); + } + } +} diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 1e425c88a6..580366a75a 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; using JetBrains.Annotations; @@ -15,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; +using osu.Game.Utils; namespace osu.Game.Configuration { @@ -228,10 +228,7 @@ namespace osu.Game.Configuration return b.Value; case IBindable u: - // An unknown (e.g. enum) generic type. - var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); - Debug.Assert(valueMethod != null); - return valueMethod.GetValue(u)!; + return BindableValueAccessor.GetValue(u); default: // fall back for non-bindable cases. diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b9a937b1a2..1b21216235 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -266,8 +266,7 @@ namespace osu.Game.Rulesets.Mods // TODO: special case for handling number types - PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable.Value))!; - property.SetValue(targetSetting, property.GetValue(sourceSetting)); + BindableValueAccessor.SetValue(targetSetting, BindableValueAccessor.GetValue(sourceSetting)); } } diff --git a/osu.Game/Utils/BindableValueAccessor.cs b/osu.Game/Utils/BindableValueAccessor.cs new file mode 100644 index 0000000000..dd097ada36 --- /dev/null +++ b/osu.Game/Utils/BindableValueAccessor.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using System.Reflection; +using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; + +namespace osu.Game.Utils +{ + internal static class BindableValueAccessor + { + private static readonly MethodInfo get_method = typeof(BindableValueAccessor).GetMethod(nameof(getValue), BindingFlags.Static | BindingFlags.NonPublic)!; + private static readonly MethodInfo set_method = typeof(BindableValueAccessor).GetMethod(nameof(setValue), BindingFlags.Static | BindingFlags.NonPublic)!; + + public static object GetValue(IBindable bindable) + { + Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(isBindableT); + if (bindableWithValueType == null) + return bindable; + + return get_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable])!; + } + + public static void SetValue(IBindable bindable, object value) + { + Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().FirstOrDefault(isBindableT); + if (bindableWithValueType == null) + return; + + set_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable, value]); + } + + private static bool isBindableT(Type type) + => type.IsGenericType + && (type.GetGenericTypeDefinition() == typeof(Bindable<>) + || type.GetGenericTypeDefinition() == typeof(IBindable<>)); + + private static object getValue(object bindable) => ((IBindable)bindable).Value!; + + private static object setValue(object bindable, object value) => ((Bindable)bindable).Value = (T)value; + } +} From 2fe229d62073a80c1a9f07155a510d9f80713584 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 25 Sep 2024 22:46:53 +0900 Subject: [PATCH 286/308] Inline condition --- osu.Game/Utils/BindableValueAccessor.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Utils/BindableValueAccessor.cs b/osu.Game/Utils/BindableValueAccessor.cs index dd097ada36..a4cd356339 100644 --- a/osu.Game/Utils/BindableValueAccessor.cs +++ b/osu.Game/Utils/BindableValueAccessor.cs @@ -16,7 +16,7 @@ namespace osu.Game.Utils public static object GetValue(IBindable bindable) { - Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(isBindableT); + Type? bindableWithValueType = bindable.GetType().GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IBindable<>)); if (bindableWithValueType == null) return bindable; @@ -25,18 +25,13 @@ namespace osu.Game.Utils public static void SetValue(IBindable bindable, object value) { - Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().FirstOrDefault(isBindableT); + Type? bindableWithValueType = bindable.GetType().EnumerateBaseTypes().SingleOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Bindable<>)); if (bindableWithValueType == null) return; set_method.MakeGenericMethod(bindableWithValueType.GenericTypeArguments[0]).Invoke(null, [bindable, value]); } - private static bool isBindableT(Type type) - => type.IsGenericType - && (type.GetGenericTypeDefinition() == typeof(Bindable<>) - || type.GetGenericTypeDefinition() == typeof(IBindable<>)); - private static object getValue(object bindable) => ((IBindable)bindable).Value!; private static object setValue(object bindable, object value) => ((Bindable)bindable).Value = (T)value; From df0966abb2475a53fed24acec5429c44dfb29bb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Sep 2024 01:11:23 +0900 Subject: [PATCH 287/308] Update velopack and switch to using async version of `WaitExitThenApplyUpdates` --- osu.Desktop/Updater/VelopackUpdateManager.cs | 15 ++++++++------- osu.Desktop/osu.Desktop.csproj | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/Updater/VelopackUpdateManager.cs b/osu.Desktop/Updater/VelopackUpdateManager.cs index ae58a8793c..7a79284533 100644 --- a/osu.Desktop/Updater/VelopackUpdateManager.cs +++ b/osu.Desktop/Updater/VelopackUpdateManager.cs @@ -66,7 +66,7 @@ namespace osu.Desktop.Updater { Activated = () => { - restartToApplyUpdate(); + Task.Run(restartToApplyUpdate); return true; } }); @@ -88,7 +88,11 @@ namespace osu.Desktop.Updater { notification = new UpdateProgressNotification { - CompletionClickAction = restartToApplyUpdate, + CompletionClickAction = () => + { + Task.Run(restartToApplyUpdate); + return true; + }, }; Schedule(() => notificationOverlay.Post(notification)); @@ -127,13 +131,10 @@ namespace osu.Desktop.Updater return true; } - private bool restartToApplyUpdate() + private async Task restartToApplyUpdate() { - // TODO: Migrate this to async flow whenever available (see https://github.com/ppy/osu/pull/28743#discussion_r1740505665). - // Currently there's an internal Thread.Sleep(300) which will cause a stutter when the user clicks to restart. - updateManager.WaitExitThenApplyUpdates(pendingUpdate?.TargetFullRelease); + await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false); Schedule(() => game.AttemptExit()); - return true; } } } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index bf5f26b352..3df8c16f08 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From 89e8baf1d35dabc6133d609e9af5e1da50cff976 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Sep 2024 16:48:42 +0900 Subject: [PATCH 288/308] Add inline comments for iOS locals --- osu.Game/Beatmaps/BeatmapImporter.cs | 1 + osu.Game/Online/BeatmapDownloadTracker.cs | 1 + osu.Game/Online/ScoreDownloadTracker.cs | 1 + osu.Game/Skinning/RealmBackedResourceStore.cs | 1 + osu.Game/Skinning/SkinManager.cs | 1 + 5 files changed, 5 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 63d8215d73..94144e4695 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -198,6 +198,7 @@ namespace osu.Game.Beatmaps if (beatmapSet.OnlineID > 0) { + // Required local for iOS. Will cause runtime crash if inlined. int onlineId = beatmapSet.OnlineID; // OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure. diff --git a/osu.Game/Online/BeatmapDownloadTracker.cs b/osu.Game/Online/BeatmapDownloadTracker.cs index c1c3d17ff6..6a2163c3a2 100644 --- a/osu.Game/Online/BeatmapDownloadTracker.cs +++ b/osu.Game/Online/BeatmapDownloadTracker.cs @@ -40,6 +40,7 @@ namespace osu.Game.Online // Used to interact with manager classes that don't support interface types. Will eventually be replaced. var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID }; + // Required local for iOS. Will cause runtime crash if inlined. int onlineId = TrackedItem.OnlineID; realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => s.OnlineID == onlineId && !s.DeletePending), (items, _) => diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index eb687a7023..5f6ba15d05 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -46,6 +46,7 @@ namespace osu.Game.Online Downloader.DownloadBegan += downloadBegan; Downloader.DownloadFailed += downloadFailed; + // Required local for iOS. Will cause runtime crash if inlined. long onlineId = TrackedItem.OnlineID; long legacyOnlineId = TrackedItem.LegacyOnlineID; string hash = TrackedItem.Hash; diff --git a/osu.Game/Skinning/RealmBackedResourceStore.cs b/osu.Game/Skinning/RealmBackedResourceStore.cs index b1289bd0c5..f41bd89b7a 100644 --- a/osu.Game/Skinning/RealmBackedResourceStore.cs +++ b/osu.Game/Skinning/RealmBackedResourceStore.cs @@ -29,6 +29,7 @@ namespace osu.Game.Skinning invalidateCache(); Debug.Assert(fileToStoragePathMapping != null); + // Required local for iOS. Will cause runtime crash if inlined. Guid id = source.ID; realmSubscription = realm?.RegisterForNotifications(r => r.All().Where(s => s.ID == id), skinChanged); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index cd431bd80c..9018c2e2c3 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -131,6 +131,7 @@ namespace osu.Game.Skinning { Realm.Run(r => { + // Required local for iOS. Will cause runtime crash if inlined. Guid currentSkinId = CurrentSkinInfo.Value.ID; // choose from only user skins, removing the current selection to ensure a new one is chosen. From b1a05f463e3bfe927dea17513953ea56cf890a38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Sep 2024 19:42:20 +0900 Subject: [PATCH 289/308] Reduce size of hidden toggle slightly --- .../UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs index ffedc9386f..521ebebf91 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs @@ -16,13 +16,15 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.None; AutoSizeAxes = Axes.None; - Size = new Vector2(100, OsuDirectorySelectorBreadcrumbDisplay.HEIGHT); + Size = new Vector2(140, OsuDirectorySelectorBreadcrumbDisplay.HEIGHT); Margin = new MarginPadding { Right = OsuDirectorySelectorBreadcrumbDisplay.HORIZONTAL_PADDING, }; Anchor = Anchor.CentreLeft; Origin = Anchor.CentreLeft; LabelTextFlowContainer.Anchor = Anchor.CentreLeft; LabelTextFlowContainer.Origin = Anchor.CentreLeft; LabelText = @"Show hidden"; + + Scale = new Vector2(0.8f); } [BackgroundDependencyLoader(true)] From f4a4807449b6ef1d8504b655a16d77a77ed79e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Aug 2024 10:29:10 +0200 Subject: [PATCH 290/308] Implement "form" file picker --- .../UserInterface/TestSceneFormControls.cs | 5 + .../UserInterfaceV2/FormFileSelector.cs | 262 ++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs index 89b4ae9f97..2a0b0515a1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFormControls.cs @@ -100,6 +100,11 @@ namespace osu.Game.Tests.Visual.UserInterface Caption = EditorSetupStrings.EnableCountdown, HintText = EditorSetupStrings.CountdownDescription, }, + new FormFileSelector + { + Caption = "Audio file", + PlaceholderText = "Select an audio file", + }, }, }, } diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs new file mode 100644 index 0000000000..66f68f3e3b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -0,0 +1,262 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public partial class FormFileSelector : CompositeDrawable, IHasCurrentValue, ICanAcceptFiles, IHasPopover + { + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public IEnumerable HandledExtensions => handledExtensions; + + private readonly string[] handledExtensions; + + /// + /// The initial path to use when displaying the . + /// + /// + /// Uses a value before the first selection is made + /// to ensure that the first selection starts at . + /// + private string? initialChooserPath; + + private readonly Bindable popoverState = new Bindable(); + + /// + /// Caption describing this file selector, displayed on top of the controls. + /// + public LocalisableString Caption { get; init; } + + /// + /// Hint text containing an extended description of this file selector, displayed in a tooltip when hovering the caption. + /// + public LocalisableString HintText { get; init; } + + /// + /// Text displayed in the selector when no file is selected. + /// + public LocalisableString PlaceholderText { get; init; } + + private Box background = null!; + + private FormFieldCaption caption = null!; + private OsuSpriteText placeholderText = null!; + private OsuSpriteText filenameText = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuGameBase game { get; set; } = null!; + + public FormFileSelector(params string[] handledExtensions) + { + this.handledExtensions = handledExtensions; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 50; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(9), + Children = new Drawable[] + { + caption = new FormFieldCaption + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Caption = Caption, + TooltipText = HintText, + }, + placeholderText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Width = 1, + Text = PlaceholderText, + Colour = colourProvider.Foreground1, + }, + filenameText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Width = 1, + }, + new SpriteIcon + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Icon = FontAwesome.Solid.FolderOpen, + Size = new Vector2(16), + Colour = colourProvider.Light1, + } + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + popoverState.BindValueChanged(_ => updateState()); + current.BindValueChanged(_ => + { + updateState(); + onFileSelected(); + }); + current.BindDisabledChanged(_ => updateState(), true); + game.RegisterImportHandler(this); + } + + private void onFileSelected() + { + if (Current.Value != null) + this.HidePopover(); + + initialChooserPath = Current.Value?.DirectoryName; + placeholderText.Alpha = Current.Value == null ? 1 : 0; + filenameText.Text = Current.Value?.Name ?? string.Empty; + background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + this.ShowPopover(); + return true; + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + updateState(); + } + + private void updateState() + { + caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; + filenameText.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; + + if (!Current.Disabled) + { + BorderThickness = IsHovered || popoverState.Value == Visibility.Visible ? 2 : 0; + BorderColour = popoverState.Value == Visibility.Visible ? colourProvider.Highlight1 : colourProvider.Light4; + + if (popoverState.Value == Visibility.Visible) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3); + else if (IsHovered) + background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4); + else + background.Colour = colourProvider.Background5; + } + else + { + background.Colour = colourProvider.Background4; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (game.IsNotNull()) + game.UnregisterImportHandler(this); + } + + Task ICanAcceptFiles.Import(params string[] paths) + { + Schedule(() => Current.Value = new FileInfo(paths.First())); + return Task.CompletedTask; + } + + Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException(); + + public Popover GetPopover() + { + var popover = new FileChooserPopover(handledExtensions, Current, initialChooserPath); + popoverState.UnbindBindings(); + popoverState.BindTo(popover.State); + return popover; + } + + private partial class FileChooserPopover : OsuPopover + { + protected override string PopInSampleName => "UI/overlay-big-pop-in"; + protected override string PopOutSampleName => "UI/overlay-big-pop-out"; + + public FileChooserPopover(string[] handledExtensions, Bindable currentFile, string? chooserPath) + : base(false) + { + Child = new Container + { + Size = new Vector2(600, 400), + Child = new OsuFileSelector(chooserPath, handledExtensions) + { + RelativeSizeAxes = Axes.Both, + CurrentFile = { BindTarget = currentFile } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Body.BorderThickness = 2; + Body.BorderColour = colourProvider.Highlight1; + } + } + } +} From 9e9bfc3721db83e5926fb6ee9bcfa0f4a2a1f684 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 12:48:50 +0900 Subject: [PATCH 291/308] Update velopack with zstd changes Closes https://github.com/ppy/osu/issues/29810. --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 3df8c16f08..342b28f5ef 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -26,7 +26,7 @@ - + From cbeeb4a2b4b5cc7485f3f44012a14103ad8f2987 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 13:43:57 +0900 Subject: [PATCH 292/308] Add basic hover states for file selector elements --- .../UserInterfaceV2/FormFileSelector.cs | 19 +++++- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 20 +------ .../OsuDirectorySelectorDirectory.cs | 6 +- .../UserInterfaceV2/OsuFileSelector.cs | 6 +- .../OsuFileSelectorBackgroundLayer.cs | 59 +++++++++++++++++++ 5 files changed, 79 insertions(+), 31 deletions(-) create mode 100644 osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index 66f68f3e3b..55cc026d7c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -24,6 +24,7 @@ using osu.Game.Database; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -254,8 +255,22 @@ namespace osu.Game.Graphics.UserInterfaceV2 [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { - Body.BorderThickness = 2; - Body.BorderColour = colourProvider.Highlight1; + Add(new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 2, + CornerRadius = 10, + BorderColour = colourProvider.Highlight1, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Transparent, + RelativeSizeAxes = Axes.Both, + }, + } + }); } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs index e91076498c..3fd1fa998f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 AddRangeInternal(new Drawable[] { - new Background + new OsuFileSelectorBackgroundLayer(0.5f) { Depth = 1 }, @@ -101,24 +101,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold)); protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? FontAwesome.Solid.Database : null; - - internal partial class Background : CompositeDrawable - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider overlayColourProvider) - { - RelativeSizeAxes = Axes.Both; - - Masking = true; - CornerRadius = 5; - - InternalChild = new Box - { - Colour = overlayColourProvider.Background3, - RelativeSizeAxes = Axes.Both, - }; - } - } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs index a36804658a..4240eb73a4 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -24,10 +23,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; - AddRangeInternal(new Drawable[] - { - new HoverClickSounds() - }); + AddInternal(new OsuFileSelectorBackgroundLayer()); Colour = colours.Orange1; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index 7ce5f63656..f54bfeebba 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 @@ -87,10 +86,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; - AddRangeInternal(new Drawable[] - { - new HoverClickSounds() - }); + AddInternal(new OsuFileSelectorBackgroundLayer()); Colour = colourProvider.Light3; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs new file mode 100644 index 0000000000..ee5e7f014d --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + internal partial class OsuFileSelectorBackgroundLayer : CompositeDrawable + { + private Box background = null!; + + private readonly float defaultAlpha; + + public OsuFileSelectorBackgroundLayer(float defaultAlpha = 0f) + { + Depth = float.MaxValue; + + this.defaultAlpha = defaultAlpha; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider overlayColourProvider) + { + RelativeSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + new HoverClickSounds(), + background = new Box + { + Alpha = defaultAlpha, + Colour = overlayColourProvider.Background3, + RelativeSizeAxes = Axes.Both, + }, + }; + } + + protected override bool OnHover(HoverEvent e) + { + background.FadeTo(1, 200, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + background.FadeTo(defaultAlpha, 500, Easing.OutQuint); + } + } +} From eacd9b9756583950a78b91affcccd92f74d2162c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 13:57:05 +0900 Subject: [PATCH 293/308] Move dependent files to namespace --- .../BackgroundLayer.cs} | 6 +++--- .../HiddenFilesToggleCheckbox.cs} | 6 +++--- .../OsuDirectorySelectorBreadcrumbDisplay.cs | 4 ++-- .../{ => FileSelection}/OsuDirectorySelectorDirectory.cs | 4 ++-- .../OsuDirectorySelectorParentDirectory.cs | 2 +- osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs | 3 ++- osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs | 5 +++-- 7 files changed, 16 insertions(+), 14 deletions(-) rename osu.Game/Graphics/UserInterfaceV2/{OsuFileSelectorBackgroundLayer.cs => FileSelection/BackgroundLayer.cs} (88%) rename osu.Game/Graphics/UserInterfaceV2/{OsuDirectorySelectorHiddenToggle.cs => FileSelection/HiddenFilesToggleCheckbox.cs} (88%) rename osu.Game/Graphics/UserInterfaceV2/{ => FileSelection}/OsuDirectorySelectorBreadcrumbDisplay.cs (97%) rename osu.Game/Graphics/UserInterfaceV2/{ => FileSelection}/OsuDirectorySelectorDirectory.cs (91%) rename osu.Game/Graphics/UserInterfaceV2/{ => FileSelection}/OsuDirectorySelectorParentDirectory.cs (92%) diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/BackgroundLayer.cs similarity index 88% rename from osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/BackgroundLayer.cs index ee5e7f014d..cd3199c6f5 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelectorBackgroundLayer.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/BackgroundLayer.cs @@ -9,15 +9,15 @@ using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { - internal partial class OsuFileSelectorBackgroundLayer : CompositeDrawable + internal partial class BackgroundLayer : CompositeDrawable { private Box background = null!; private readonly float defaultAlpha; - public OsuFileSelectorBackgroundLayer(float defaultAlpha = 0f) + public BackgroundLayer(float defaultAlpha = 0f) { Depth = float.MaxValue; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/HiddenFilesToggleCheckbox.cs similarity index 88% rename from osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/HiddenFilesToggleCheckbox.cs index 521ebebf91..07d84a0095 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/HiddenFilesToggleCheckbox.cs @@ -8,11 +8,11 @@ using osu.Game.Overlays; using osuTK; using osuTK.Graphics; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { - internal partial class OsuDirectorySelectorHiddenToggle : OsuCheckbox + internal partial class HiddenFilesToggleCheckbox : OsuCheckbox { - public OsuDirectorySelectorHiddenToggle() + public HiddenFilesToggleCheckbox() { RelativeSizeAxes = Axes.None; AutoSizeAxes = Axes.None; diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs similarity index 97% rename from osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs index 3fd1fa998f..aeeda82bfb 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorBreadcrumbDisplay.cs @@ -13,7 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { internal partial class OsuDirectorySelectorBreadcrumbDisplay : DirectorySelectorBreadcrumbDisplay { @@ -80,7 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 AddRangeInternal(new Drawable[] { - new OsuFileSelectorBackgroundLayer(0.5f) + new BackgroundLayer(0.5f) { Depth = 1 }, diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorDirectory.cs similarity index 91% rename from osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorDirectory.cs index 4240eb73a4..0da4e1929f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorDirectory.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { internal partial class OsuDirectorySelectorDirectory : DirectorySelectorDirectory { @@ -23,7 +23,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; - AddInternal(new OsuFileSelectorBackgroundLayer()); + AddInternal(new BackgroundLayer()); Colour = colours.Orange1; } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorParentDirectory.cs similarity index 92% rename from osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs rename to osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorParentDirectory.cs index d274a0ecfe..e5e1e0b7f3 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorParentDirectory.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelection/OsuDirectorySelectorParentDirectory.cs @@ -6,7 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Game.Overlays; -namespace osu.Game.Graphics.UserInterfaceV2 +namespace osu.Game.Graphics.UserInterfaceV2.FileSelection { internal partial class OsuDirectorySelectorParentDirectory : OsuDirectorySelectorDirectory { diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs index 85599a5d45..65ffdcaa5b 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2.FileSelection; using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 @@ -57,7 +58,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.Both, }, - new OsuDirectorySelectorHiddenToggle + new HiddenFilesToggleCheckbox { Current = { BindTarget = ShowHiddenItems }, }, diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs index f54bfeebba..c7b559d9ed 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2.FileSelection; using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 @@ -58,7 +59,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 { RelativeSizeAxes = Axes.Both, }, - new OsuDirectorySelectorHiddenToggle + new HiddenFilesToggleCheckbox { Current = { BindTarget = ShowHiddenItems }, }, @@ -86,7 +87,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Flow.AutoSizeAxes = Axes.X; Flow.Height = OsuDirectorySelector.ITEM_HEIGHT; - AddInternal(new OsuFileSelectorBackgroundLayer()); + AddInternal(new BackgroundLayer()); Colour = colourProvider.Light3; } From b2983e25629e407c3045a65bded33bebe43931cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 14:21:16 +0900 Subject: [PATCH 294/308] Update shader preloader with missing shader usages --- osu.Game/Screens/Loader.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 57e3998646..f64ae196a0 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -118,13 +118,20 @@ namespace osu.Game.Screens { loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); - - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"TriangleBorder")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"FastCircle")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"CircularProgress")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPath")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPathBackground")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SaturationSelectorBackground")); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"HueSelectorBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); + + loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); + loadTargets.Add(manager.Load(@"LogoAnimation", @"LogoAnimation")); } protected virtual bool AllLoaded => loadTargets.All(s => s.IsLoaded); From 4205a21c0c595be177cd46b00f7e9b23d1361c69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 16:11:24 +0900 Subject: [PATCH 295/308] Add one more shader usage --- osu.Game/Screens/Loader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index f64ae196a0..4a59b180f5 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -127,6 +127,8 @@ namespace osu.Game.Screens loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPathBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SaturationSelectorBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"HueSelectorBackground")); + // Ruleset local shader usage (should probably move somewhere else). + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SpinnerGlow")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); From 5be63ee304e54852723cd4b043459b4aa733eb9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 16:16:17 +0900 Subject: [PATCH 296/308] Reorganise with ruleset shader separated out --- osu.Game/Screens/Loader.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 4a59b180f5..d71ee05b27 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -118,7 +118,7 @@ namespace osu.Game.Screens { loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"TriangleBorder")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"FastCircle")); @@ -127,13 +127,11 @@ namespace osu.Game.Screens loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPathBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SaturationSelectorBackground")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"HueSelectorBackground")); + loadTargets.Add(manager.Load(@"LogoAnimation", @"LogoAnimation")); + // Ruleset local shader usage (should probably move somewhere else). loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SpinnerGlow")); - - loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); - loadTargets.Add(manager.Load(@"LogoAnimation", @"LogoAnimation")); } protected virtual bool AllLoaded => loadTargets.All(s => s.IsLoaded); From 21796900e2eeed8e8b9d707b285eca5a4184f6fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Sep 2024 09:26:08 +0200 Subject: [PATCH 297/308] Fix code quality naming issue --- osu.Game/Utils/GeometryUtils.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Utils/GeometryUtils.cs b/osu.Game/Utils/GeometryUtils.cs index e9e79deb49..eac86a9c02 100644 --- a/osu.Game/Utils/GeometryUtils.cs +++ b/osu.Game/Utils/GeometryUtils.cs @@ -309,21 +309,21 @@ namespace osu.Game.Utils { // Using Welzl's algorithm to find the minimum enclosing circle // https://www.geeksforgeeks.org/minimum-enclosing-circle-using-welzls-algorithm/ - List P = points.ToList(); + List p = points.ToList(); var stack = new Stack<(Vector2?, int)>(); var r = new List(3); (Vector2, float) d = (Vector2.Zero, 0); - stack.Push((null, P.Count)); + stack.Push((null, p.Count)); while (stack.Count > 0) { - // n represents the number of points in P that are not yet processed. - // p represents the point that was randomly picked to process. - (Vector2? p, int n) = stack.Pop(); + // `n` represents the number of points in P that are not yet processed. + // `point` represents the point that was randomly picked to process. + (Vector2? point, int n) = stack.Pop(); - if (!p.HasValue) + if (!point.HasValue) { // Base case when all points processed or |R| = 3 if (n == 0 || r.Count == 3) @@ -334,30 +334,30 @@ namespace osu.Game.Utils // Pick a random point randomly int idx = RNG.Next(n); - p = P[idx]; + point = p[idx]; // Put the picked point at the end of P since it's more efficient than // deleting from the middle of the list - (P[idx], P[n - 1]) = (P[n - 1], P[idx]); + (p[idx], p[n - 1]) = (p[n - 1], p[idx]); // Schedule processing of p after we get the MEC circle d from the set of points P - {p} - stack.Push((p, n)); + stack.Push((point, n)); // Get the MEC circle d from the set of points P - {p} stack.Push((null, n - 1)); } else { // If d contains p, return d - if (isInside(d, p.Value)) + if (isInside(d, point.Value)) continue; // Remove points from R that were added in a deeper recursion // |R| = |P| - |stack| - n - int removeCount = r.Count - (P.Count - stack.Count - n); + int removeCount = r.Count - (p.Count - stack.Count - n); r.RemoveRange(r.Count - removeCount, removeCount); // Otherwise, must be on the boundary of the MEC - r.Add(p.Value); + r.Add(point.Value); // Return the MEC for P - {p} and R U {p} stack.Push((null, n - 1)); } From cb51e12d1393dab1a9c00ec542889b64fa5d73a2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 27 Sep 2024 16:24:51 +0900 Subject: [PATCH 298/308] Fix iOS CI build --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4abd55e3f4..6fbb74dfba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,5 +135,8 @@ jobs: - name: Install .NET Workloads run: dotnet workload install maui-ios + - name: Select Xcode 16 + run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer + - name: Build run: dotnet build -c Debug osu.iOS From 1dd6082aa9ea6c06647ddadfaddd1ae1720b9fba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 16:56:22 +0900 Subject: [PATCH 299/308] Rename method to be more appropriate --- .../HUD/JudgementCounter/JudgementCountController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs index 2562e26127..c00cb3487b 100644 --- a/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs +++ b/osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCountController.cs @@ -52,16 +52,16 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter { base.LoadComplete(); - scoreProcessor.OnResetFromReplayFrame += updateAllCounts; + scoreProcessor.OnResetFromReplayFrame += updateAllCountsFromReplayFrame; scoreProcessor.NewJudgement += judgement => updateCount(judgement, false); scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true); } - private bool hasUpdatedCounts; + private bool hasUpdatedCountsFromReplayFrame; - private void updateAllCounts() + private void updateAllCountsFromReplayFrame() { - if (hasUpdatedCounts) + if (hasUpdatedCountsFromReplayFrame) return; foreach (var kvp in scoreProcessor.Statistics) @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter count.ResultCount.Value = kvp.Value; } - hasUpdatedCounts = true; + hasUpdatedCountsFromReplayFrame = true; } private void updateCount(JudgementResult judgement, bool revert) From 92ee86e3dd210e2b05877e2477ee27d54a086be3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 17:40:06 +0900 Subject: [PATCH 300/308] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index c7ce707562..6b42258b49 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index bb20125282..8acd1deff1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From 371cee1617854c76f8cfd98427aae75bdf460ac1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 17:41:27 +0900 Subject: [PATCH 301/308] Consume framework change to avoid weird unbind flow --- osu.Game/OsuGame.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1af86b2d83..44ba78762a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -446,11 +446,7 @@ namespace osu.Game case LinkAction.SearchBeatmapSet: if (link.Argument is LocalisableString localisable) - { - var localised = Localisation.GetLocalisedBindableString(localisable); - SearchBeatmapSet(localised.Value); - localised.UnbindAll(); - } + SearchBeatmapSet(Localisation.GetLocalisedString(localisable)); else SearchBeatmapSet(argString); From e7c44512066454c21726a4c24b6e93233571e25b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Sep 2024 18:20:16 +0900 Subject: [PATCH 302/308] Reduce brightness of hover effect --- osu.Game/Overlays/Mods/ModCustomisationHeader.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs index 1d40fb3f5c..54fbd37dbe 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationHeader.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationHeader.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Mods [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; - public readonly Bindable ExpandedState = new Bindable(ModCustomisationPanelState.Collapsed); + public readonly Bindable ExpandedState = new Bindable(); private readonly ModCustomisationPanel panel; @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Mods hoverBackground = new Box { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(80).Opacity(180), + Colour = OsuColour.Gray(50), Blending = BlendingParameters.Additive, Alpha = 0, }, @@ -134,16 +134,13 @@ namespace osu.Game.Overlays.Mods if (panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed) panel.ExpandedState.Value = ModCustomisationPanelState.Expanded; - hoverBackground.FadeIn(200); - + hoverBackground.FadeTo(0.4f, 200, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - if (Enabled.Value) - hoverBackground.FadeOut(200); - + hoverBackground.FadeOut(200, Easing.OutQuint); base.OnHoverLost(e); } } From eb725ec1fb19c2348c9a8c8442644ef4f8cc33ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Sep 2024 12:13:11 +0200 Subject: [PATCH 303/308] Nudge test coverage to also cover discovered fail case --- .../Visual/Editing/TestSceneComposerSelection.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 765d7ee21e..13d5a7e3b2 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -244,11 +244,8 @@ namespace osu.Game.Tests.Visual.Editing InputManager.PressKey(Key.ControlLeft); }); AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5))); - AddStep("end dragging", () => - { - InputManager.ReleaseButton(MouseButton.Left); - InputManager.ReleaseKey(Key.ControlLeft); - }); + AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left)); + AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft)); AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4)); From d60733175563068265fb76baf649646e6843c726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Sep 2024 12:15:08 +0200 Subject: [PATCH 304/308] Fix control-drag selection expansion deselecting object if control is released over one of the blueprints --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 9776e64855..30c1258f93 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -432,7 +432,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool endClickSelection(MouseButtonEvent e) { // If already handled a selection, double-click, or drag, we don't want to perform a mouse up / click action. - if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint) return true; + if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint || wasDragStarted) return true; if (e.Button != MouseButton.Left) return false; @@ -448,7 +448,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } - if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) + if (selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) { // If a click occurred and was handled by the currently selected blueprint but didn't result in a drag, // cycle between other blueprints which are also under the cursor. From f473f4398c90e477686b48307ae9f9ec26e3a906 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 28 Sep 2024 22:37:16 +0300 Subject: [PATCH 305/308] Fix text in FormFileSelector bleeding through the border --- osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs index 55cc026d7c..42bf9c7b9f 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs @@ -244,6 +244,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 Child = new Container { Size = new Vector2(600, 400), + // simplest solution to avoid underlying text to bleed through the bottom border + // https://github.com/ppy/osu/pull/30005#issuecomment-2378884430 + Padding = new MarginPadding { Bottom = 1 }, Child = new OsuFileSelector(chooserPath, handledExtensions) { RelativeSizeAxes = Axes.Both, From 3fac9baa9f97e1918d3bfb3760bf3c157be2fb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 08:38:11 +0200 Subject: [PATCH 306/308] Add test steps demonstrating failure case --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6b8fa94336..aae0648157 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -175,6 +175,20 @@ namespace osu.Game.Tests.Visual.SongSelect increaseModSpeed(); AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed); + OsuModDoubleTime dtWithAdjustPitch = new OsuModDoubleTime + { + SpeedChange = { Value = 1.05 }, + AdjustPitch = { Value = true }, + }; + changeMods(dtWithAdjustPitch); + + decreaseModSpeed(); + AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); + + decreaseModSpeed(); + AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); + AddAssert("half time has adjust pitch active", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); + void increaseModSpeed() => AddStep("increase mod speed", () => { InputManager.PressKey(Key.ControlLeft); From 23b8354af4b5564b94f4d17282f517f71f8db398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 08:46:45 +0200 Subject: [PATCH 307/308] Add more test steps demonstrating another failure case --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index aae0648157..3a95aca6b9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -189,6 +189,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005)); AddAssert("half time has adjust pitch active", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.True); + AddStep("turn off adjust pitch", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value = false); + + increaseModSpeed(); + AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0); + + increaseModSpeed(); + AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005)); + AddAssert("double time has adjust pitch inactive", () => songSelect!.Mods.Value.OfType().Single().AdjustPitch.Value, () => Is.False); + void increaseModSpeed() => AddStep("increase mod speed", () => { InputManager.PressKey(Key.ControlLeft); From 5e5bb49cd8d8726223fca0eacd531fc797fd4c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Sep 2024 08:47:02 +0200 Subject: [PATCH 308/308] Fix rate change hotkeys sometimes losing track of adjust pitch setting Fixes https://osu.ppy.sh/community/forums/topics/1983327. The cause of the bug is a bit convoluted, and stems from the fact that the mod select overlay controls all of the game-global mod instances if present. `ModSpeedHotkeyHandler` would store the last spotted instance of a rate adjust mod - which in this case is a problem, because on deselection of a mod, the mod select overlay resets its settings to defaults: https://github.com/ppy/osu/blob/a258059d4338b999b8e065e48b952d14a6d14fb8/osu.Game/Overlays/Mods/ModSelectOverlay.cs#L424-L425 A way to defend against this is a clone, but this reveals another issue, in that the existing code was *relying* on the reference to the mod remaining the same in any other case, to read the latest valid settings of the mod. This basically only mattered in the edge case wherein Double Time would swap places with Half Time and vice versa (think [0.95,1.05] range). Therefore, track mod settings too explicitly to ensure that the stored clone is as up-to-date as possible. --- osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs index af64002bcf..c4cd44705e 100644 --- a/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs +++ b/osu.Game/Screens/Select/ModSpeedHotkeyHandler.cs @@ -27,6 +27,7 @@ namespace osu.Game.Screens.Select private OnScreenDisplay? onScreenDisplay { get; set; } private ModRateAdjust? lastActiveRateAdjustMod; + private ModSettingChangeTracker? settingChangeTracker; protected override void LoadComplete() { @@ -34,10 +35,19 @@ namespace osu.Game.Screens.Select selectedMods.BindValueChanged(val => { - lastActiveRateAdjustMod = val.NewValue.OfType().SingleOrDefault() ?? lastActiveRateAdjustMod; + storeLastActiveRateAdjustMod(); + + settingChangeTracker?.Dispose(); + settingChangeTracker = new ModSettingChangeTracker(val.NewValue); + settingChangeTracker.SettingChanged += _ => storeLastActiveRateAdjustMod(); }, true); } + private void storeLastActiveRateAdjustMod() + { + lastActiveRateAdjustMod = (ModRateAdjust?)selectedMods.Value.OfType().SingleOrDefault()?.DeepClone() ?? lastActiveRateAdjustMod; + } + public bool ChangeSpeed(double delta, IEnumerable availableMods) { double targetSpeed = (selectedMods.Value.OfType().SingleOrDefault()?.SpeedChange.Value ?? 1) + delta;