From c1743dbe1df7994f425d118c19c44ee8ea8c126c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Jun 2023 16:53:29 +0900 Subject: [PATCH 001/896] Select all text when popover is initially shown Depends on https://github.com/ppy/osu-framework/pull/5823. --- osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 5a1fbbee1e..62925ff708 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -328,7 +328,7 @@ namespace osu.Game.Screens.Edit.Compose.Components BeatDivisor.BindValueChanged(_ => updateState(), true); divisorTextBox.OnCommit += (_, _) => setPresets(); - Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox)); + divisorTextBox.SelectAll(); } private void setPresets() From 49598774e0886279bd17caacdcbe4a57af646481 Mon Sep 17 00:00:00 2001 From: Givikap120 Date: Thu, 24 Aug 2023 15:00:32 +0300 Subject: [PATCH 002/896] Added a "Adjust pitch" switch Adjust pitch switch in DoubleTime and HalfTime mods Also Nightcore and Daycore is now inherit from RateAdjust --- osu.Game/Rulesets/Mods/ModDaycore.cs | 26 +++++++++++++++++++++- osu.Game/Rulesets/Mods/ModDoubleTime.cs | 26 ++++++++++++++++++++++ osu.Game/Rulesets/Mods/ModHalfTime.cs | 26 ++++++++++++++++++++++ osu.Game/Rulesets/Mods/ModNightcore.cs | 29 ++++++++++++++++++++++++- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 5 +---- 5 files changed, 106 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index de1a5ab56c..c5810cc9e5 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -5,16 +5,26 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Configuration; namespace osu.Game.Rulesets.Mods { - public abstract class ModDaycore : ModHalfTime + public abstract class ModDaycore : ModRateAdjust { public override string Name => "Daycore"; public override string Acronym => "DC"; public override IconUsage? Icon => null; + public override ModType Type => ModType.DifficultyReduction; public override LocalisableString Description => "Whoaaaaa..."; + [SettingSource("Speed decrease", "The actual decrease to apply")] + public override BindableNumber SpeedChange { get; } = new BindableDouble(0.75) + { + MinValue = 0.5, + MaxValue = 0.99, + Precision = 0.01, + }; + private readonly BindableNumber tempoAdjust = new BindableDouble(1); private readonly BindableNumber freqAdjust = new BindableDouble(1); @@ -33,5 +43,19 @@ namespace osu.Game.Rulesets.Mods track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } + + public override double ScoreMultiplier + { + get + { + // Round to the nearest multiple of 0.1. + double value = (int)(SpeedChange.Value * 10) / 10.0; + + // Offset back to 0. + value -= 1; + + return 1 + value; + } + } } } diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 733610c040..ad196ec83c 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.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.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -25,6 +26,31 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; + private IAdjustableAudioComponent? track; + + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] + public virtual BindableBool AdjustPitch { get; } = new BindableBool(false); + + protected ModDoubleTime() + { + AdjustPitch.BindValueChanged(adjustPitchChanged); + } + + private void adjustPitchChanged(ValueChangedEvent adjustPitchSetting) + { + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + } + + private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) + => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; + + public override void ApplyToTrack(IAdjustableAudioComponent track) + { + this.track = track; + AdjustPitch.TriggerChange(); + } + public override double ScoreMultiplier { get diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index 06c7750035..047dd4bd04 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.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.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -25,6 +26,31 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; + private IAdjustableAudioComponent? track; + + [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] + public virtual BindableBool AdjustPitch { get; } = new BindableBool(false); + + protected ModHalfTime() + { + AdjustPitch.BindValueChanged(adjustPitchChanged); + } + + private void adjustPitchChanged(ValueChangedEvent adjustPitchSetting) + { + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + } + + private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) + => adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo; + + public override void ApplyToTrack(IAdjustableAudioComponent track) + { + this.track = track; + AdjustPitch.TriggerChange(); + } + public override double ScoreMultiplier { get diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 9b1f7d5cf7..80b9314aec 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects; @@ -19,12 +20,38 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mods { - public abstract class ModNightcore : ModDoubleTime + public abstract class ModNightcore : ModRateAdjust { public override string Name => "Nightcore"; public override string Acronym => "NC"; public override IconUsage? Icon => OsuIcon.ModNightcore; + public override ModType Type => ModType.DifficultyIncrease; public override LocalisableString Description => "Uguuuuuuuu..."; + + [SettingSource("Speed increase", "The actual increase to apply")] + public override BindableNumber SpeedChange { get; } = new BindableDouble(1.5) + { + MinValue = 1.01, + MaxValue = 2, + Precision = 0.01, + }; + + public override double ScoreMultiplier + { + get + { + // Round to the nearest multiple of 0.1. + double value = (int)(SpeedChange.Value * 10) / 10.0; + + // Offset back to 0. + value -= 1; + + // Each 0.1 multiple changes score multiplier by 0.02. + value /= 5; + + return 1 + value; + } + } } public abstract partial class ModNightcore : ModNightcore, IApplicableToDrawableRuleset diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 7b55ba4ad0..6bf4b65e3b 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -13,10 +13,7 @@ namespace osu.Game.Rulesets.Mods public abstract BindableNumber SpeedChange { get; } - public virtual void ApplyToTrack(IAdjustableAudioComponent track) - { - track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); - } + public abstract void ApplyToTrack(IAdjustableAudioComponent track); public virtual void ApplyToSample(IAdjustableAudioComponent sample) { From 5241634c491e058aae4026a0a81a34d80e1dffde Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 31 Aug 2023 16:31:54 +0900 Subject: [PATCH 003/896] Add one more test --- .../TestSceneHoldNoteInput.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 77db1b0bd8..ebf25d7a9b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -192,6 +192,25 @@ namespace osu.Game.Rulesets.Mania.Tests assertTailJudgement(HitResult.Miss); } + /// + /// -----[ ]----- + /// xox o + /// + [Test] + public void TestPressAtStartThenReleaseAndImmediatelyRepress() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 1), + new ManiaReplayFrame(time_head + 2, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + /// /// -----[ ]----- /// xo x o From c60f13dd92133d8d7359450cf1e3284bbfda2bc3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 31 Aug 2023 17:30:27 +0900 Subject: [PATCH 004/896] Remove mania hold note ticks --- .../TestSceneHoldNoteInput.cs | 57 --------- .../Objects/Drawables/DrawableHoldNote.cs | 17 --- .../Objects/Drawables/DrawableHoldNoteTick.cs | 110 ------------------ osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 34 ------ .../Objects/HoldNoteTick.cs | 19 --- osu.Game.Rulesets.Mania/UI/Column.cs | 1 - osu.Game.Rulesets.Mania/UI/Stage.cs | 4 - 7 files changed, 242 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs delete mode 100644 osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index ebf25d7a9b..1bf768012e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -54,7 +54,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); assertNoteJudgement(HitResult.IgnoreMiss); } @@ -73,7 +72,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Perfect); assertNoteJudgement(HitResult.IgnoreHit); } @@ -92,7 +90,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); assertNoteJudgement(HitResult.IgnoreMiss); } @@ -111,7 +108,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -129,7 +125,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -149,7 +144,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -169,7 +163,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Perfect); } @@ -188,7 +181,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -227,7 +219,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -247,7 +238,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -265,7 +255,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -283,7 +272,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -377,7 +365,6 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); assertHitObjectJudgement(note, HitResult.Good); @@ -424,7 +411,6 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); assertHitObjectJudgement(note, HitResult.Great); @@ -444,7 +430,6 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Meh); } @@ -495,42 +480,6 @@ namespace osu.Game.Rulesets.Mania.Tests .All(j => j.Type.IsHit())); } - [Test] - public void TestHitTailBeforeLastTick() - { - const int tick_rate = 8; - const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate; - const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1); - - var beatmap = new Beatmap - { - HitObjects = - { - new HoldNote - { - StartTime = time_head, - Duration = time_tail - time_head, - Column = 0, - } - }, - BeatmapInfo = - { - Difficulty = new BeatmapDifficulty { SliderTickRate = tick_rate }, - Ruleset = new ManiaRuleset().RulesetInfo - }, - }; - - performTest(new List - { - new ManiaReplayFrame(time_head, ManiaAction.Key1), - new ManiaReplayFrame(time_last_tick - 5) - }, beatmap); - - assertHeadJudgement(HitResult.Perfect); - assertLastTickJudgement(HitResult.LargeTickMiss); - assertTailJudgement(HitResult.Ok); - } - [Test] public void TestZeroLength() { @@ -570,12 +519,6 @@ namespace osu.Game.Rulesets.Mania.Tests private void assertNoteJudgement(HitResult result) => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result)); - private void assertTickJudgement(HitResult result) - => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Select(j => j.Type), () => Does.Contain(result)); - - private void assertLastTickJudgement(HitResult result) - => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result)); - private ScoreAccessibleReplayPlayer currentPlayer = null!; private void performTest(List frames, Beatmap? beatmap = null) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index c3fec92b92..3032a3ee39 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private Container headContainer; private Container tailContainer; - private Container tickContainer; private PausableSkinnableSound slidingSample; @@ -110,7 +109,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { RelativeSizeAxes = Axes.X }, - tickContainer = new Container { RelativeSizeAxes = Axes.Both }, tailContainer = new Container { RelativeSizeAxes = Axes.Both }, slidingSample = new PausableSkinnableSound { Looping = true } }); @@ -118,7 +116,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables maskedContents.AddRange(new[] { bodyPiece.CreateProxy(), - tickContainer.CreateProxy(), tailContainer.CreateProxy(), }); } @@ -153,10 +150,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables case DrawableHoldNoteTail tail: tailContainer.Child = tail; break; - - case DrawableHoldNoteTick tick: - tickContainer.Add(tick); - break; } } @@ -165,7 +158,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.ClearNestedHitObjects(); headContainer.Clear(false); tailContainer.Clear(false); - tickContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -177,9 +169,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables case HeadNote head: return new DrawableHoldNoteHead(head); - - case HoldNoteTick tick: - return new DrawableHoldNoteTick(tick); } return base.CreateNestedHitObject(hitObject); @@ -266,12 +255,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - foreach (var tick in tickContainer) - { - if (!tick.Judged) - tick.MissForcefully(); - } - if (Tail.IsHit) ApplyResult(r => r.Type = r.Judgement.MaxResult); else diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs deleted file mode 100644 index ce6a83f79f..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ /dev/null @@ -1,110 +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; -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; - -namespace osu.Game.Rulesets.Mania.Objects.Drawables -{ - /// - /// Visualises a hit object. - /// - public partial class DrawableHoldNoteTick : DrawableManiaHitObject - { - /// - /// References the time at which the user started holding the hold note. - /// - private Func holdStartTime; - - private Container glowContainer; - - public DrawableHoldNoteTick() - : this(null) - { - } - - public DrawableHoldNoteTick(HoldNoteTick hitObject) - : base(hitObject) - { - Anchor = Anchor.TopCentre; - Origin = Anchor.TopCentre; - - RelativeSizeAxes = Axes.X; - } - - [BackgroundDependencyLoader] - private void load() - { - AddInternal(glowContainer = new CircularContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - AccentColour.BindValueChanged(colour => - { - glowContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 2f, - Roundness = 15f, - Colour = colour.NewValue.Opacity(0.3f) - }; - }, true); - } - - protected override void OnApply() - { - base.OnApply(); - - Debug.Assert(ParentHitObject != null); - - var holdNote = (DrawableHoldNote)ParentHitObject; - holdStartTime = () => holdNote.HoldStartTime; - } - - protected override void OnFree() - { - base.OnFree(); - - holdStartTime = null; - } - - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - if (Time.Current < HitObject.StartTime) - return; - - double? startTime = holdStartTime?.Invoke(); - - if (startTime == null || startTime > HitObject.StartTime) - ApplyResult(r => r.Type = r.Judgement.MinResult); - else - ApplyResult(r => r.Type = r.Judgement.MaxResult); - } - } -} diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index c367886efe..333912dd0c 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -6,8 +6,6 @@ using System.Collections.Generic; using System.Threading; using osu.Game.Audio; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -83,25 +81,10 @@ namespace osu.Game.Rulesets.Mania.Objects public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; - /// - /// The time between ticks of this hold. - /// - private double tickSpacing = 50; - - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; - } - protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { base.CreateNestedHitObjects(cancellationToken); - createTicks(cancellationToken); - AddNested(Head = new HeadNote { StartTime = StartTime, @@ -117,23 +100,6 @@ namespace osu.Game.Rulesets.Mania.Objects }); } - private void createTicks(CancellationToken cancellationToken) - { - if (tickSpacing == 0) - return; - - for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing) - { - cancellationToken.ThrowIfCancellationRequested(); - - AddNested(new HoldNoteTick - { - StartTime = t, - Column = Column - }); - } - } - public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs deleted file mode 100644 index e5c5260a49..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs +++ /dev/null @@ -1,19 +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.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.Judgements; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Mania.Objects -{ - /// - /// A scoring tick of a hold note. - /// - public class HoldNoteTick : ManiaHitObject - { - public override Judgement CreateJudgement() => new HoldNoteTickJudgement(); - - protected override HitWindows CreateHitWindows() => HitWindows.Empty; - } -} diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index f38571a6d3..1c4f0462ec 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -112,7 +112,6 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); - RegisterPool(50, 250); } private void onSourceChanged() diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 4382f8e84a..fa9af6d157 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -195,10 +195,6 @@ namespace osu.Game.Rulesets.Mania.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; - // Tick judgements should not display text. - if (judgedObject is DrawableHoldNoteTick) - return; - judgements.Clear(false); judgements.Add(judgementPool.Get(j => { From 0fd445f913d44c2e89c2a291147167ac14c2735f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 1 Sep 2023 20:42:27 +0900 Subject: [PATCH 005/896] Adjust test to fail --- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 1bf768012e..5bc477535d 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -200,7 +200,9 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); + assertCombo(1); assertTailJudgement(HitResult.Meh); + assertCombo(1); } /// @@ -519,6 +521,9 @@ namespace osu.Game.Rulesets.Mania.Tests private void assertNoteJudgement(HitResult result) => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result)); + private void assertCombo(int combo) + => AddAssert($"combo is {combo}", () => currentPlayer.ScoreProcessor.Combo.Value, () => Is.EqualTo(combo)); + private ScoreAccessibleReplayPlayer currentPlayer = null!; private void performTest(List frames, Beatmap? beatmap = null) From 292273edbe6e14671c6ef03e6a44504b20736c23 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 23 Sep 2023 06:31:26 +0300 Subject: [PATCH 006/896] Add test scene --- osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs index ac80463d3a..97e1cae11c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs @@ -64,7 +64,8 @@ namespace osu.Game.Tests.Visual.Online new[] { "Plain", "This is plain comment" }, new[] { "Pinned", "This is pinned comment" }, new[] { "Link", "Please visit https://osu.ppy.sh" }, - + new[] { "Big Image", "![](Backgrounds/bg1)" }, + new[] { "Small Image", "![](Cursor/cursortrail)" }, new[] { "Heading", @"# Heading 1 From 793d1bf9706dcbf6b26363b3085283898bc3033c Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 25 Sep 2023 12:52:49 +0200 Subject: [PATCH 007/896] Add ability to search for difficulty names --- .../Select/Carousel/CarouselBeatmap.cs | 2 + osu.Game/Screens/Select/FilterCriteria.cs | 47 ++++++++++++------- osu.Game/Screens/Select/FilterQueryParser.cs | 19 ++++++++ 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 6917bd1da2..f433e71cc3 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -64,6 +64,8 @@ namespace osu.Game.Screens.Select.Carousel if (!match) return false; + match &= criteria.DifficultySearchTerms.All(term => term.Matches(BeatmapInfo.DifficultyName)); + if (criteria.SearchTerms.Length > 0) { var searchableTerms = BeatmapInfo.GetSearchableTerms(); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index a2ae114126..aa3909fa0e 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -46,6 +46,7 @@ namespace osu.Game.Screens.Select }; public OptionalTextFilter[] SearchTerms = Array.Empty(); + public OptionalTextFilter[] DifficultySearchTerms = Array.Empty(); public RulesetInfo? Ruleset; public bool AllowConvertedBeatmaps; @@ -64,24 +65,7 @@ namespace osu.Game.Screens.Select { searchText = value; - List terms = new List(); - - string remainingText = value; - - // First handle quoted segments to ensure we keep inline spaces in exact matches. - foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)")) - { - terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value }); - remainingText = remainingText.Replace(quotedSegment.Value, string.Empty); - } - - // Then handle the rest splitting on any spaces. - terms.AddRange(remainingText.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(s => new OptionalTextFilter - { - SearchTerm = s - })); - - SearchTerms = terms.ToArray(); + SearchTerms = getTermsFromSearchText(value); SearchNumber = null; @@ -90,6 +74,11 @@ namespace osu.Game.Screens.Select } } + public string DifficultySearchText + { + set => DifficultySearchTerms = getTermsFromSearchText(value); + } + /// /// Hashes from the to filter to. /// @@ -97,6 +86,28 @@ namespace osu.Game.Screens.Select public IRulesetFilterCriteria? RulesetCriteria { get; set; } + private static OptionalTextFilter[] getTermsFromSearchText(string searchText) + { + List terms = new List(); + + string remainingText = searchText; + + // First handle quoted segments to ensure we keep inline spaces in exact matches. + foreach (Match quotedSegment in Regex.Matches(searchText, "(\"[^\"]+\"[!]?)")) + { + terms.Add(new OptionalTextFilter { SearchTerm = quotedSegment.Value }); + remainingText = remainingText.Replace(quotedSegment.Value, string.Empty); + } + + // Then handle the rest splitting on any spaces. + terms.AddRange(remainingText.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(s => new OptionalTextFilter + { + SearchTerm = s + })); + + return terms.ToArray(); + } + public struct OptionalRange : IEquatable> where T : struct { diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 1238173b41..797493189d 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -19,8 +19,27 @@ namespace osu.Game.Screens.Select @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex difficulty_query_syntax_regex = new Regex( + @"(\s|^)((\[(?>\[(?)|[^[\]]+|\](?<-level>))*(?(level)(?!))\](\s|$))|(\[.*))", + RegexOptions.Compiled); + internal static void ApplyQueries(FilterCriteria criteria, string query) { + foreach (Match match in difficulty_query_syntax_regex.Matches(query)) + { + // Trim the first character because it's always '[' (ignoring spaces) + string cleanDifficultyQuery = match.Value.Trim(' ')[1..]; + + if (cleanDifficultyQuery.EndsWith(']')) + cleanDifficultyQuery = cleanDifficultyQuery[..^1]; + + criteria.DifficultySearchText = cleanDifficultyQuery; + + // Insert whitespace if necessary so that the words before and after the difficulty query aren't joined together. + bool insertWhitespace = match.Value.StartsWith(' ') && match.Value.EndsWith(' '); + query = query.Replace(match.Value, insertWhitespace ? " " : ""); + } + foreach (Match match in query_syntax_regex.Matches(query)) { string key = match.Groups["key"].Value.ToLowerInvariant(); From ab57cbf6f5a1d438963469f4dea27974755559e7 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 25 Sep 2023 12:53:17 +0200 Subject: [PATCH 008/896] Add `TestSceneDifficultySearch` --- .../SongSelect/TestSceneDifficultySearch.cs | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 osu.Game.Tests/SongSelect/TestSceneDifficultySearch.cs diff --git a/osu.Game.Tests/SongSelect/TestSceneDifficultySearch.cs b/osu.Game.Tests/SongSelect/TestSceneDifficultySearch.cs new file mode 100644 index 0000000000..63963d1101 --- /dev/null +++ b/osu.Game.Tests/SongSelect/TestSceneDifficultySearch.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.SongSelect +{ + public partial class TestSceneDifficultySearch : OsuTestScene + { + private static readonly (string title, string difficultyName)[] beatmaps = + { + ("Title1", "Diff1"), + ("Title1", "Diff2"), + ("My[Favourite]Song", "Expert"), + ("Title", "My Favourite Diff"), + ("Another One", "diff ]with [[] brackets]]]"), + }; + + [TestCase("[1]", new[] { 0 })] + [TestCase("[1", new[] { 0 })] + [TestCase("My[Favourite", new[] { 2 })] + [TestCase("My[Favourite]", new[] { 2 })] + [TestCase("My[Favourite]Song", new[] { 2 })] + [TestCase("Favourite]", new[] { 2 })] + [TestCase("[Diff", new[] { 0, 1, 3, 4 })] + [TestCase("[Diff]", new[] { 0, 1, 3, 4 })] + [TestCase("[Favourite]", new[] { 3 })] + [TestCase("Title1 [Diff]", new[] { 0, 1 })] + [TestCase("Title1[Diff]", new int[] { })] + [TestCase("[diff ]with]", new[] { 4 })] + [TestCase("[diff ]with [[] brackets]]]]", new[] { 4 })] + public void TestDifficultySearch(string query, int[] expectedBeatmapIndexes) + { + var carouselBeatmaps = createCarouselBeatmaps().ToList(); + + AddStep("filter beatmaps", () => + { + var criteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(criteria, query); + carouselBeatmaps.ForEach(b => b.Filter(criteria)); + }); + + AddAssert("filtered correctly", () => carouselBeatmaps.All(b => + { + int index = carouselBeatmaps.IndexOf(b); + + bool filtered = b.Filtered.Value; + + return filtered != expectedBeatmapIndexes.Contains(index); + })); + } + + private static IEnumerable createCarouselBeatmaps() + { + return beatmaps.Select(info => new CarouselBeatmap(new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Title = info.title + }, + DifficultyName = info.difficultyName + })); + } + } +} From 74896fd6b2f271dfd5ec01c01aa04526a72125c3 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Mon, 25 Sep 2023 13:38:47 +0200 Subject: [PATCH 009/896] Fix multiple difficulty search queries not working in some cases. --- osu.Game/Screens/Select/FilterQueryParser.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 797493189d..58e25adb5b 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -25,8 +25,12 @@ namespace osu.Game.Screens.Select internal static void ApplyQueries(FilterCriteria criteria, string query) { - foreach (Match match in difficulty_query_syntax_regex.Matches(query)) + while (true) { + var match = difficulty_query_syntax_regex.Matches(query).FirstOrDefault(); + + if (match is null) break; + // Trim the first character because it's always '[' (ignoring spaces) string cleanDifficultyQuery = match.Value.Trim(' ')[1..]; From 55c623ff02d12c6d2d2d995903f9eb707310210d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 14:47:55 +0900 Subject: [PATCH 010/896] Apply NRT to `TestSceneSliderInput` --- .../TestSceneSliderInput.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 644524f532..4020d22910 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -33,7 +31,11 @@ namespace osu.Game.Rulesets.Osu.Tests private const double time_during_slide_4 = 3800; private const double time_slider_end = 4000; - private List judgementResults; + private ScoreAccessibleReplayPlayer currentPlayer = null!; + + private const float slider_path_length = 25; + + private readonly List judgementResults = new List(); [Test] public void TestPressBothKeysSimultaneouslyAndReleaseOne() @@ -333,10 +335,6 @@ namespace osu.Game.Rulesets.Osu.Tests private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; - private ScoreAccessibleReplayPlayer currentPlayer; - - private const float slider_path_length = 25; - private void performTest(List frames) { AddStep("load player", () => @@ -375,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; LoadScreen(currentPlayer = p); - judgementResults = new List(); + judgementResults.Clear(); }); AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); From 135d2497e7fe918c6a7bb9cc2fc1c542c40223c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 15:15:39 +0900 Subject: [PATCH 011/896] Add gameplay test coverage around last tick of slider This includes proposed changes as per https://github.com/ppy/osu/issues/22805#issuecomment-1740377493. --- .../TestSceneSliderInput.cs | 81 +++++++++++++++---- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 4020d22910..a9d8860369 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -37,6 +37,58 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly List judgementResults = new List(); + [Test] + public void TestTrackingAtTailButNotLastTick() + { + performTest(new List + { + new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start }, + new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 50 }, + new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 350 }, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_slider_start + 380 }, + }, new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 10f, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(slider_path_length * 10, 0), + new Vector2(slider_path_length * 10, slider_path_length), + new Vector2(0, slider_path_length), + }), + }); + + AddAssert("Full judgement awarded", assertMaxJudge); + } + + [Test] + public void TestTrackingAtLastTickButNotTail() + { + performTest(new List + { + new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start }, + new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 50 }, + new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 350 }, + new OsuReplayFrame { Position = new Vector2(100, 0), Actions = { OsuAction.RightButton }, Time = time_slider_start + 380 }, + }, new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 10f, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(slider_path_length * 10, 0), + new Vector2(slider_path_length * 10, slider_path_length), + new Vector2(0, slider_path_length), + }), + }); + + AddAssert("Full judgement awarded", assertMaxJudge); + } + [Test] public void TestPressBothKeysSimultaneouslyAndReleaseOne() { @@ -335,26 +387,25 @@ namespace osu.Game.Rulesets.Osu.Tests private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; - private void performTest(List frames) + private void performTest(List frames, Slider? slider = null) { + slider ??= new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 0.1f, + Path = new SliderPath(PathType.PerfectCurve, new[] + { + Vector2.Zero, + new Vector2(slider_path_length, 0), + }, slider_path_length), + }; + AddStep("load player", () => { Beatmap.Value = CreateWorkingBeatmap(new Beatmap { - HitObjects = - { - new Slider - { - StartTime = time_slider_start, - Position = new Vector2(0, 0), - SliderVelocityMultiplier = 0.1f, - Path = new SliderPath(PathType.PerfectCurve, new[] - { - Vector2.Zero, - new Vector2(slider_path_length, 0), - }, slider_path_length), - } - }, + HitObjects = { slider }, BeatmapInfo = { Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, From e3695d2be017d7acb88b6e1d57e5aaffe3f672ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 16:08:28 +0900 Subject: [PATCH 012/896] Adjust slider judgement logic to allow tracking anywhere after last tick --- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/DrawableSliderTail.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 77e60a1690..f4138bdf91 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -256,7 +256,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (userTriggered || Time.Current < HitObject.EndTime) + if (userTriggered || !TailCircle.Judged) return; // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 9fbc97c484..6344a6d30f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; @@ -125,8 +126,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!userTriggered && timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); + if (userTriggered) + return; + + if (timeOffset >= 0 && Tracking) + ApplyResult(r => r.Type = r.Judgement.MaxResult); + else if (timeOffset >= -SliderEventGenerator.LAST_TICK_OFFSET) + ApplyResult(r => r.Type = r.Judgement.MinResult); } protected override void OnApply() From dd6d09189e21d7a58df891823ba1daf9e8625c83 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 16:20:21 +0900 Subject: [PATCH 013/896] Remove usage of `LastTick` in osu! ruleset --- osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs | 2 +- .../Objects/Drawables/DrawableSliderTail.cs | 6 ++++-- osu.Game.Rulesets.Osu/Objects/Slider.cs | 6 +----- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 5 ----- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 78062a0632..c465ab8732 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Mods }); break; - case SliderEventType.LastTick: + case SliderEventType.Tail: AddNested(TailCircle = new StrictTrackingSliderTailCircle(this) { RepeatIndex = e.SpanIndex, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 6344a6d30f..7daab820dd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -129,9 +129,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered) return; - if (timeOffset >= 0 && Tracking) + // The player needs to have engaged in tracking at any point after the tail leniency cutoff. + // An actual tick miss should only occur if reaching the tick itself. + if (timeOffset >= SliderEventGenerator.TAIL_LENIENCY && Tracking) ApplyResult(r => r.Type = r.Judgement.MaxResult); - else if (timeOffset >= -SliderEventGenerator.LAST_TICK_OFFSET) + else if (timeOffset >= 0) ApplyResult(r => r.Type = r.Judgement.MinResult); } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 443e4229d2..c62659d67a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -204,11 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects }); break; - case SliderEventType.LastTick: - // Of note, we are directly mapping LastTick (instead of `SliderEventType.Tail`) to SliderTailCircle. - // It is required as difficulty calculation and gameplay relies on reading this value. - // (although it is displayed in classic skins, which may be a concern). - // If this is to change, we should revisit this. + case SliderEventType.Tail: AddNested(TailCircle = new SliderTailCircle(this) { RepeatIndex = e.SpanIndex, diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index fb81936837..54d2afb444 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -2,16 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { - /// - /// Note that this should not be used for timing correctness. - /// See usage in for more information. - /// public class SliderTailCircle : SliderEndCircle { public SliderTailCircle(Slider slider) From 62bcb6284232833fbec4f30a8f79bfa635771ddf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 16:44:04 +0900 Subject: [PATCH 014/896] Document remaining pieces of `LastTick` and add back legacy prefix for generation flow --- .../Beatmaps/SliderEventGenerationTest.cs | 4 ++-- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs index 37a91c8611..c7cf3fe956 100644 --- a/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs +++ b/osu.Game.Tests/Beatmaps/SliderEventGenerationTest.cs @@ -87,8 +87,8 @@ namespace osu.Game.Tests.Beatmaps { var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray(); - Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LastTick)); - Assert.That(events[2].Time, Is.EqualTo(span_duration + SliderEventGenerator.LAST_TICK_OFFSET)); + Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick)); + Assert.That(events[2].Time, Is.EqualTo(span_duration + SliderEventGenerator.TAIL_LENIENCY)); } [Test] diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index b3477a5fde..42c422a637 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -16,8 +16,12 @@ namespace osu.Game.Rulesets.Objects /// until the true end of the slider. This very small amount of leniency makes it easier to jump away from fast sliders to the next hit object. /// /// After discussion on how this should be handled going forward, players have unanimously stated that this lenience should remain in some way. + /// These days, this is implemented in the drawable implementation of Slider in the osu! ruleset. + /// + /// We need to keep the *only* for osu!catch conversion, which relies on it to generate tiny ticks + /// correctly. /// - public const double LAST_TICK_OFFSET = -36; + public const double TAIL_LENIENCY = -36; public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, CancellationToken cancellationToken = default) @@ -84,14 +88,14 @@ namespace osu.Game.Rulesets.Objects int finalSpanIndex = spanCount - 1; double finalSpanStartTime = startTime + finalSpanIndex * spanDuration; - double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) + LAST_TICK_OFFSET); + double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) + TAIL_LENIENCY); double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration; if (spanCount % 2 == 0) finalProgress = 1 - finalProgress; yield return new SliderEventDescriptor { - Type = SliderEventType.LastTick, + Type = SliderEventType.LegacyLastTick, SpanIndex = finalSpanIndex, SpanStartTime = finalSpanStartTime, Time = finalSpanEndTime, @@ -183,9 +187,10 @@ namespace osu.Game.Rulesets.Objects Tick, /// - /// Occurs just before the tail. See . + /// Occurs just before the tail. See . + /// Should generally be ignored. /// - LastTick, + LegacyLastTick, Head, Tail, Repeat From 22cb168c0ffef3e1cef825e15f2e030135ac5039 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 17:17:04 +0900 Subject: [PATCH 015/896] Add test coverage of tracking inside lenience period but not at tick or end --- .../TestSceneSliderInput.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index a9d8860369..cf02e0eb32 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -37,6 +37,33 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly List judgementResults = new List(); + [Test] + public void TestTrackingBetweenLastTickAndTail() + { + performTest(new List + { + new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start }, + new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 50 }, + new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 480 }, + new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 500 }, + new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.RightButton }, Time = time_slider_start + 520 }, + }, new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 10f, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(slider_path_length * 10, 0), + new Vector2(slider_path_length * 10, slider_path_length), + new Vector2(0, slider_path_length), + }), + }); + + AddAssert("Full judgement awarded", assertMaxJudge); + } + [Test] public void TestTrackingAtTailButNotLastTick() { From 23c20ca5f44c6eb1a56ce5f6c932dd951e5945b5 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Fri, 29 Sep 2023 13:44:11 +0200 Subject: [PATCH 016/896] Add xmldocs --- osu.Game/Screens/Select/FilterCriteria.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index aa3909fa0e..1bcff601e5 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -46,6 +46,10 @@ namespace osu.Game.Screens.Select }; public OptionalTextFilter[] SearchTerms = Array.Empty(); + + /// + /// Search terms that are used for searching difficulty names. + /// public OptionalTextFilter[] DifficultySearchTerms = Array.Empty(); public RulesetInfo? Ruleset; @@ -74,6 +78,10 @@ namespace osu.Game.Screens.Select } } + /// + /// Extracts the search terms from the provided + /// and stores them in . + /// public string DifficultySearchText { set => DifficultySearchTerms = getTermsFromSearchText(value); From c927f90a4834ccf2d5f7c8108feeebfb6ebbfd66 Mon Sep 17 00:00:00 2001 From: Pasi4K5 Date: Fri, 29 Sep 2023 13:44:42 +0200 Subject: [PATCH 017/896] Replace regex with a custom algorithm and update test scene. --- .../SongSelect/TestSceneDifficultySearch.cs | 5 +- osu.Game/Screens/Select/FilterQueryParser.cs | 90 ++++++++++++++----- 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/SongSelect/TestSceneDifficultySearch.cs b/osu.Game.Tests/SongSelect/TestSceneDifficultySearch.cs index 63963d1101..f013d5c19a 100644 --- a/osu.Game.Tests/SongSelect/TestSceneDifficultySearch.cs +++ b/osu.Game.Tests/SongSelect/TestSceneDifficultySearch.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.SongSelect ("Title1", "Diff2"), ("My[Favourite]Song", "Expert"), ("Title", "My Favourite Diff"), - ("Another One", "diff ]with [[] brackets]]]"), + ("Another One", "diff ]with [[ brackets]]]"), }; [TestCase("[1]", new[] { 0 })] @@ -34,7 +34,8 @@ namespace osu.Game.Tests.SongSelect [TestCase("Title1 [Diff]", new[] { 0, 1 })] [TestCase("Title1[Diff]", new int[] { })] [TestCase("[diff ]with]", new[] { 4 })] - [TestCase("[diff ]with [[] brackets]]]]", new[] { 4 })] + [TestCase("[diff ]with [[ brackets]]]]", new[] { 4 })] + [TestCase("[diff] another [brackets]", new[] { 4 })] public void TestDifficultySearch(string query, int[] expectedBeatmapIndexes) { var carouselBeatmaps = createCarouselBeatmaps().ToList(); diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 58e25adb5b..08fe86713a 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -19,30 +19,9 @@ namespace osu.Game.Screens.Select @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*""[!]?)|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex difficulty_query_syntax_regex = new Regex( - @"(\s|^)((\[(?>\[(?)|[^[\]]+|\](?<-level>))*(?(level)(?!))\](\s|$))|(\[.*))", - RegexOptions.Compiled); - internal static void ApplyQueries(FilterCriteria criteria, string query) { - while (true) - { - var match = difficulty_query_syntax_regex.Matches(query).FirstOrDefault(); - - if (match is null) break; - - // Trim the first character because it's always '[' (ignoring spaces) - string cleanDifficultyQuery = match.Value.Trim(' ')[1..]; - - if (cleanDifficultyQuery.EndsWith(']')) - cleanDifficultyQuery = cleanDifficultyQuery[..^1]; - - criteria.DifficultySearchText = cleanDifficultyQuery; - - // Insert whitespace if necessary so that the words before and after the difficulty query aren't joined together. - bool insertWhitespace = match.Value.StartsWith(' ') && match.Value.EndsWith(' '); - query = query.Replace(match.Value, insertWhitespace ? " " : ""); - } + criteria.DifficultySearchText = extractDifficultySearchText(ref query); foreach (Match match in query_syntax_regex.Matches(query)) { @@ -57,6 +36,73 @@ namespace osu.Game.Screens.Select criteria.SearchText = query; } + /// + /// Extracts and returns the difficulty search text between square brackets. + /// + /// The search query. The difficulty search text will be removed from the query. + /// The difficulty search text (without the square brackets). + private static string extractDifficultySearchText(ref string query) + { + var openingBracketIndexes = new List(); + var closingBracketIndexes = new List(); + + populateIndexLists(ref query); + + return performExtraction(ref query); + + void populateIndexLists(ref string query) + { + bool currentlyBetweenBrackets = false; + + for (int i = 0; i < query.Length; i++) + { + switch (query[i]) + { + case '[' when !currentlyBetweenBrackets && (i == 0 || query[i - 1] == ' '): + currentlyBetweenBrackets = true; + openingBracketIndexes.Add(i + 1); + break; + + case ']' when currentlyBetweenBrackets && (i == query.Length - 1 || query[i + 1] == ' '): + currentlyBetweenBrackets = false; + closingBracketIndexes.Add(i); + break; + } + } + + if (currentlyBetweenBrackets) + { + // If there is no "]" closing the current difficulty search query, append it. + query += ']'; + closingBracketIndexes.Add(query.Length - 1); + } + } + + string performExtraction(ref string query) + { + var searchTexts = new List(); + string originalQuery = query; + + for (int i = 0; i < openingBracketIndexes.Count; i++) + { + int startIndex = openingBracketIndexes[i]; + int endIndex = closingBracketIndexes[i]; + + string searchText = originalQuery[startIndex..endIndex]; + + searchTexts.Add(searchText); + + query = query + .Replace($" [{searchText}]", "") + .Replace($"[{searchText}] ", "") + .Replace($"[{searchText}]", "") + .Replace($" [{searchText}] ", " "); + } + + return string.Join(' ', searchTexts); + } + } + private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, Operator op) { switch (key) From 2410036003e75c4af046f5b15ea00f469328e679 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Sep 2023 17:31:02 +0900 Subject: [PATCH 018/896] Refactor tests to be easier to visually understand --- .../TestSceneSliderInput.cs | 91 ++++++------------- 1 file changed, 27 insertions(+), 64 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index cf02e0eb32..bacc4fb218 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -37,16 +38,18 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly List judgementResults = new List(); - [Test] - public void TestTrackingBetweenLastTickAndTail() + [TestCase(300, false)] + [TestCase(200, true)] + [TestCase(150, true)] + [TestCase(120, true)] + [TestCase(60, true)] + [TestCase(10, true)] + public void TestTailLeniency(float finalPosition, bool hit) { performTest(new List { new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start }, - new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 50 }, - new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 480 }, - new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 500 }, - new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.RightButton }, Time = time_slider_start + 520 }, + new OsuReplayFrame { Position = new Vector2(finalPosition, slider_path_length * 3), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 20 }, }, new Slider { StartTime = time_slider_start, @@ -56,64 +59,18 @@ namespace osu.Game.Rulesets.Osu.Tests { Vector2.Zero, new Vector2(slider_path_length * 10, 0), - new Vector2(slider_path_length * 10, slider_path_length), - new Vector2(0, slider_path_length), + new Vector2(slider_path_length * 10, slider_path_length * 3), + new Vector2(0, slider_path_length * 3), }), - }); + }, 240, 1); - AddAssert("Full judgement awarded", assertMaxJudge); - } - - [Test] - public void TestTrackingAtTailButNotLastTick() - { - performTest(new List + if (hit) { - new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start }, - new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 50 }, - new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 350 }, - new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_slider_start + 380 }, - }, new Slider - { - StartTime = time_slider_start, - Position = new Vector2(0, 0), - SliderVelocityMultiplier = 10f, - Path = new SliderPath(PathType.Linear, new[] - { - Vector2.Zero, - new Vector2(slider_path_length * 10, 0), - new Vector2(slider_path_length * 10, slider_path_length), - new Vector2(0, slider_path_length), - }), - }); - - AddAssert("Full judgement awarded", assertMaxJudge); - } - - [Test] - public void TestTrackingAtLastTickButNotTail() - { - performTest(new List - { - new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start }, - new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 50 }, - new OsuReplayFrame { Position = new Vector2(200, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 350 }, - new OsuReplayFrame { Position = new Vector2(100, 0), Actions = { OsuAction.RightButton }, Time = time_slider_start + 380 }, - }, new Slider - { - StartTime = time_slider_start, - Position = new Vector2(0, 0), - SliderVelocityMultiplier = 10f, - Path = new SliderPath(PathType.Linear, new[] - { - Vector2.Zero, - new Vector2(slider_path_length * 10, 0), - new Vector2(slider_path_length * 10, slider_path_length), - new Vector2(0, slider_path_length), - }), - }); - - AddAssert("Full judgement awarded", assertMaxJudge); + AddAssert("Tracking retained", assertMaxJudge); + AddAssert("Full judgement awarded", assertMaxJudge); + } + else + AddAssert("Tracking dropped", assertMidSliderJudgementFail); } [Test] @@ -414,7 +371,7 @@ namespace osu.Game.Rulesets.Osu.Tests private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; - private void performTest(List frames, Slider? slider = null) + private void performTest(List frames, Slider? slider = null, double? bpm = null, int? tickRate = null) { slider ??= new Slider { @@ -430,14 +387,20 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("load player", () => { + var cpi = new ControlPointInfo(); + + if (bpm != null) + cpi.Add(0, new TimingControlPoint { BeatLength = 60000 / bpm.Value }); + Beatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = { slider }, BeatmapInfo = { - Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, - Ruleset = new OsuRuleset().RulesetInfo + Difficulty = new BeatmapDifficulty { SliderTickRate = tickRate ?? 3 }, + Ruleset = new OsuRuleset().RulesetInfo, }, + ControlPointInfo = cpi, }); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); From 07207ffc322108beb76a6ca9bee9566581eec150 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 13:31:42 +0900 Subject: [PATCH 019/896] Fix hitsounds playing too early on fast sliders --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 9 ++++++++- .../Objects/Drawables/DrawableSlider.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index bacc4fb218..d920dfa859 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -46,11 +46,13 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(10, true)] public void TestTailLeniency(float finalPosition, bool hit) { + Slider slider; + performTest(new List { new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start }, new OsuReplayFrame { Position = new Vector2(finalPosition, slider_path_length * 3), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 20 }, - }, new Slider + }, slider = new Slider { StartTime = time_slider_start, Position = new Vector2(0, 0), @@ -71,6 +73,11 @@ namespace osu.Game.Rulesets.Osu.Tests } else AddAssert("Tracking dropped", assertMidSliderJudgementFail); + + // Even if the last tick is hit early, the slider should always execute its final judgement at its endtime. + // If not, hitsounds will not play on time. + AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0); + AddAssert("Slider judged at end time", () => judgementResults.Last().TimeAbsolute, () => Is.EqualTo(slider.EndTime)); } [Test] diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index f4138bdf91..5571332076 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -256,7 +256,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (userTriggered || !TailCircle.Judged) + if (userTriggered || !TailCircle.Judged || Time.Current < HitObject.EndTime) return; // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. From b683d55023c639f908f94a9d083563647756f379 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 13:41:49 +0900 Subject: [PATCH 020/896] Add a couple of extra tests --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index d920dfa859..ff6b68e1d2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -44,6 +44,8 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(120, true)] [TestCase(60, true)] [TestCase(10, true)] + [TestCase(0, true)] + [TestCase(-30, false)] public void TestTailLeniency(float finalPosition, bool hit) { Slider slider; From 589abe2c52c221ad07a4f4869672f8e4ae579711 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 15:28:29 +0900 Subject: [PATCH 021/896] Add note about broken test --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index ff6b68e1d2..68f635baa1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(120, true)] [TestCase(60, true)] [TestCase(10, true)] - [TestCase(0, true)] + // [TestCase(0, true)] headless test doesn't run at high enough precision for this to always enter a tracking state in time. [TestCase(-30, false)] public void TestTailLeniency(float finalPosition, bool hit) { From 393ec119dd3b6db47421f7fc2ca1e373bf9d3cf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 13:42:30 +0900 Subject: [PATCH 022/896] Add test coverage of very short sliders --- .../TestSceneSliderInput.cs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 68f635baa1..a3b9d40601 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.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 NUnit.Framework; @@ -38,6 +39,44 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly List judgementResults = new List(); + // Making these too short causes breakage from frames not being processed fast enough. + // To keep things simple, these tests are crafted to always be >16ms length. + // If sliders shorter than this are ever used in gameplay it will probably break things and we can revisit. + [TestCase(80, 0)] + [TestCase(80, 1)] + [TestCase(80, 10)] + public void TestVeryShortSlider(float sliderLength, int repeatCount) + { + Slider slider; + + performTest(new List + { + new OsuReplayFrame { Position = new Vector2(10, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start - 10 }, + new OsuReplayFrame { Position = new Vector2(10, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 2000 }, + }, slider = new Slider + { + StartTime = time_slider_start, + Position = new Vector2(0, 0), + SliderVelocityMultiplier = 10f, + RepeatCount = repeatCount, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(sliderLength, 0), + }), + }, 240, 1); + + AddAssert("Slider is longer than one frame", () => slider.Duration / (slider.RepeatCount + 1), () => Is.GreaterThan(1000 / 60f)); + AddAssert("Slider is shorter than lenience", () => slider.Duration / (slider.RepeatCount + 1), () => Is.LessThan(Math.Abs(SliderEventGenerator.TAIL_LENIENCY))); + + assertAllMaxJudgements(); + + // Even if the last tick is hit early, the slider should always execute its final judgement at its endtime. + // If not, hitsounds will not play on time. + AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0); + AddAssert("Slider judged at end time", () => judgementResults.Last().TimeAbsolute, () => Is.EqualTo(slider.EndTime)); + } + [TestCase(300, false)] [TestCase(200, true)] [TestCase(150, true)] @@ -70,7 +109,6 @@ namespace osu.Game.Rulesets.Osu.Tests if (hit) { - AddAssert("Tracking retained", assertMaxJudge); AddAssert("Full judgement awarded", assertMaxJudge); } else From 3a124a99ce49a39448725122c08eb80012819b8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 14:34:26 +0900 Subject: [PATCH 023/896] Improve test output for judgement checking --- .../TestSceneSliderInput.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index a3b9d40601..e98e7f45f6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -108,9 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests }, 240, 1); if (hit) - { - AddAssert("Full judgement awarded", assertMaxJudge); - } + assertAllMaxJudgements(); else AddAssert("Tracking dropped", assertMidSliderJudgementFail); @@ -129,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -171,7 +169,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 }, }); - AddAssert("Tracking retained", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -192,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -213,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -386,7 +384,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.199f), Actions = { OsuAction.LeftButton }, Time = time_slider_end }, }); - AddAssert("Tracking kept", assertMaxJudge); + assertAllMaxJudgements(); } /// @@ -410,7 +408,13 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Tracking dropped", assertMidSliderJudgementFail); } - private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); + private void assertAllMaxJudgements() + { + AddAssert("All judgements max", () => + { + return judgementResults.Select(j => (j.HitObject, j.Type)); + }, () => Is.EqualTo(judgementResults.Select(j => (j.HitObject, j.Judgement.MaxResult)))); + } private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit; From d19cdbdefb731664dda045512f0f7b07486cde75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 22:31:47 +0200 Subject: [PATCH 024/896] Add `InvitePlayer` method to multiplayer server --- osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs | 7 +++++++ osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 ++ osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs | 10 ++++++++++ 3 files changed, 19 insertions(+) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index b7a5faf7c9..64cd6df24d 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -99,5 +99,12 @@ namespace osu.Game.Online.Multiplayer /// /// The item to remove. Task RemovePlaylistItem(long playlistItemId); + + /// + /// Invites a player to the current room. + /// + /// + /// + Task InvitePlayer(int userId); } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 5716b7ad3b..957f55406a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -260,6 +260,8 @@ namespace osu.Game.Online.Multiplayer protected abstract Task LeaveRoomInternal(); + public abstract Task InvitePlayer(int userId); + /// /// Change the current settings. /// diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 8ff0ce4065..ebe89cf018 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -106,6 +106,16 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } + public override Task InvitePlayer(int userId) + { + if (!IsConnected.Value) + return Task.CompletedTask; + + Debug.Assert(connection != null); + + return connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId); + } + public override Task TransferHost(int userId) { if (!IsConnected.Value) From 574dc67a9ed12762d4a56b2bf20ab5cb128df7e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 22:53:28 +0200 Subject: [PATCH 025/896] Add `Invited` task to multiplayer client --- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 7 +++++++ osu.Game/Online/Multiplayer/MultiplayerClient.cs | 15 +++++++++++++++ .../Online/Multiplayer/OnlineMultiplayerClient.cs | 1 + .../Visual/Multiplayer/TestMultiplayerClient.cs | 5 +++++ 4 files changed, 28 insertions(+) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 995bac1af5..f59ded93f8 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -42,6 +42,13 @@ namespace osu.Game.Online.Multiplayer /// The user. Task UserKicked(MultiplayerRoomUser user); + /// + /// Signals that a user has been invited into a multiplayer room. + /// + /// Id of user that invited the player. + /// The room the user got invited to. + Task Invited(int invitedBy, MultiplayerRoom room); + /// /// Signal that the host of the room has changed. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 957f55406a..e438f9f96d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -442,6 +442,21 @@ namespace osu.Game.Online.Multiplayer return handleUserLeft(user, UserKicked); } + async Task IMultiplayerClient.Invited(int invitedBy, MultiplayerRoom room) + { + var user = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false); + + if (user == null) return; + + Scheduler.Add(() => + { + PostNotification?.Invoke(new SimpleNotification + { + Text = "You got invited into a multiplayer match by " + user.Username + "!", + }); + }); + } + private void addUserToAPIRoom(MultiplayerRoomUser user) { Debug.Assert(APIRoom != null); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index ebe89cf018..0e327bbc83 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -50,6 +50,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); connection.On(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked); + connection.On(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited); connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c27e30d5bb..d44eff47a3 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -263,6 +263,11 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override Task InvitePlayer(int userId) + { + return Task.CompletedTask; + } + public override Task TransferHost(int userId) { userId = clone(userId); From 7629b725a29834339bc029d1bd754c19e75a5199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 22:55:53 +0200 Subject: [PATCH 026/896] Add invite button to UserPanel context menu --- osu.Game/Users/UserPanel.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index e2dc511391..29a6c65555 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; @@ -18,6 +19,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Resources.Localisation.Web; using osu.Game.Localisation; +using osu.Game.Online.Multiplayer; namespace osu.Game.Users { @@ -61,6 +63,9 @@ namespace osu.Game.Users [Resolved] protected OsuColour Colours { get; private set; } = null!; + [Resolved] + private MultiplayerClient multiplayerClient { get; set; } = null!; + [BackgroundDependencyLoader] private void load() { @@ -117,6 +122,15 @@ namespace osu.Game.Users })); } + if ( + User.IsOnline && + multiplayerClient.Room != null && + multiplayerClient.Room.Users.All(u => u.UserID != User.Id) + ) + { + items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(User.Id))); + } + return items.ToArray(); } } From 251e4d4de9d1bb5e44353ffec1f5bdacc546a3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 23:10:29 +0200 Subject: [PATCH 027/896] Add localisation for inviting a player --- osu.Game/Localisation/ContextMenuStrings.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Localisation/ContextMenuStrings.cs b/osu.Game/Localisation/ContextMenuStrings.cs index 8bc213016b..029fba67d8 100644 --- a/osu.Game/Localisation/ContextMenuStrings.cs +++ b/osu.Game/Localisation/ContextMenuStrings.cs @@ -19,6 +19,11 @@ namespace osu.Game.Localisation /// public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap"); + /// + /// "Invite player" + /// + public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite player"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } From e81695bcacdde50284133c01db2fd33ecafabac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 23:10:51 +0200 Subject: [PATCH 028/896] Display avatar in invitation notification --- .../TestSceneNotificationOverlay.cs | 17 ++++ .../Online/Multiplayer/MultiplayerClient.cs | 7 +- osu.Game/Overlays/NotificationOverlay.cs | 2 +- .../Notifications/UserAvatarNotification.cs | 78 +++++++++++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Overlays/Notifications/UserAvatarNotification.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 4d3ae079e3..07bd722322 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -4,13 +4,17 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Database; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -31,6 +35,8 @@ namespace osu.Game.Tests.Visual.UserInterface public double TimeToCompleteProgress { get; set; } = 2000; + private readonly UserLookupCache userLookupCache = new TestUserLookupCache(); + [SetUp] public void SetUp() => Schedule(() => { @@ -60,6 +66,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep(@"simple #2", sendAmazingNotification); AddStep(@"progress #1", sendUploadProgress); AddStep(@"progress #2", sendDownloadProgress); + AddStep(@"User notification", sendUserNotification); checkProgressingCount(2); @@ -537,6 +544,16 @@ namespace osu.Game.Tests.Visual.UserInterface progressingNotifications.Add(n); } + private async void sendUserNotification() + { + var user = await userLookupCache.GetUserAsync(0).ConfigureAwait(true); + if (user == null) return; + + var n = new UserAvatarNotification(user, $"{user.Username} is telling you to NOT download Haitai!"); + + notificationOverlay.Post(n); + } + private void sendUploadProgress() { var n = new ProgressNotification diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index e438f9f96d..bb953bae58 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -450,10 +450,9 @@ namespace osu.Game.Online.Multiplayer Scheduler.Add(() => { - PostNotification?.Invoke(new SimpleNotification - { - Text = "You got invited into a multiplayer match by " + user.Username + "!", - }); + PostNotification?.Invoke( + new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!") + ); }); } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 6e0ea23dd1..67cf868fb0 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -113,7 +113,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Children = new[] { - new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }), + new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification), typeof(UserAvatarNotification) }), new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }), } } diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs new file mode 100644 index 0000000000..191d63a76f --- /dev/null +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -0,0 +1,78 @@ +// 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; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users.Drawables; + +namespace osu.Game.Overlays.Notifications +{ + public partial class UserAvatarNotification : Notification + { + private LocalisableString text; + + public override LocalisableString Text + { + get => text; + set + { + text = value; + if (textDrawable != null) + textDrawable.Text = text; + } + } + + private TextFlowContainer? textDrawable; + + private APIUser user; + + public UserAvatarNotification(APIUser user, LocalisableString text) + { + this.user = user; + Text = text; + } + + private DrawableAvatar? avatar; + + protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IconContent.Masking = true; + + // Workaround for the corner radius on parent's mask breaking if we add masking to IconContent + IconContent.CornerRadius = 6; + + IconContent.AddRange(new Drawable[] + { + new Box() + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + }); + + avatar = new DrawableAvatar(user) + { + FillMode = FillMode.Fill, + }; + LoadComponentAsync(avatar, IconContent.Add); + + Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium)) + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Text = text + }); + } + } +} From 3879775219991cf44e9f8f44e320dc233637f286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 2 Oct 2023 23:20:24 +0200 Subject: [PATCH 029/896] Add room name to invite notification --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index bb953bae58..6e46a5a3b9 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -451,7 +451,7 @@ namespace osu.Game.Online.Multiplayer Scheduler.Add(() => { PostNotification?.Invoke( - new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!") + new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match:\"{room.Settings.Name}\"!") ); }); } From 8e73dbc894d4297b46aaf1a881d40acda797445f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 3 Oct 2023 01:22:25 +0200 Subject: [PATCH 030/896] Load api room before displaying notification --- .../Online/Multiplayer/IMultiplayerClient.cs | 5 ++- .../Online/Multiplayer/MultiplayerClient.cs | 41 +++++++++++++++++-- .../Multiplayer/OnlineMultiplayerClient.cs | 2 +- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index f59ded93f8..ba9e8a237c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -46,8 +46,9 @@ namespace osu.Game.Online.Multiplayer /// Signals that a user has been invited into a multiplayer room. /// /// Id of user that invited the player. - /// The room the user got invited to. - Task Invited(int invitedBy, MultiplayerRoom room); + /// Id of the room the user got invited to. + /// Password to join the room. + Task Invited(int invitedBy, long roomID, string password); /// /// Signal that the host of the room has changed. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 6e46a5a3b9..61c2fa495a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Development; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Database; @@ -30,6 +31,8 @@ namespace osu.Game.Online.Multiplayer { public Action? PostNotification { protected get; set; } + public Action? InviteAccepted { protected get; set; } + /// /// Invoked when any change occurs to the multiplayer room. /// @@ -442,20 +445,50 @@ namespace osu.Game.Online.Multiplayer return handleUserLeft(user, UserKicked); } - async Task IMultiplayerClient.Invited(int invitedBy, MultiplayerRoom room) + async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) { - var user = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false); + var loadUserTask = userLookupCache.GetUserAsync(invitedBy); + var loadRoomTask = loadRoom(roomID); - if (user == null) return; + await Task.WhenAll(loadUserTask, loadRoomTask).ConfigureAwait(false); + + APIUser? apiUser = loadUserTask.GetResultSafely(); + Room? apiRoom = loadRoomTask.GetResultSafely(); + + if (apiUser == null || apiRoom == null) return; Scheduler.Add(() => { PostNotification?.Invoke( - new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match:\"{room.Settings.Name}\"!") + new UserAvatarNotification(apiUser, $"{apiUser.Username} invited you to a multiplayer match:\"{apiRoom.Name}\"!") + { + Activated = () => + { + InviteAccepted?.Invoke(apiRoom, password); + return true; + } + } ); }); } + private Task loadRoom(long id) + { + return Task.Run(() => + { + var t = new TaskCompletionSource(); + var request = new GetRoomRequest(id); + + request.Success += room => t.TrySetResult(room); + + request.Failure += e => t.TrySetResult(null); + + API.Queue(request); + + return t.Task; + }); + } + private void addUserToAPIRoom(MultiplayerRoomUser user) { Debug.Assert(APIRoom != null); diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 0e327bbc83..5b5741ef1c 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -50,7 +50,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); connection.On(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked); - connection.On(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited); + connection.On(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited); connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); From a171fa7649c7c4e59544a3c7a167694fba51bfe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 3 Oct 2023 01:31:30 +0200 Subject: [PATCH 031/896] Join multiplayer match when clicking the invite notification --- osu.Game/OsuGame.cs | 22 +++++++++++++++++++ .../OnlinePlay/Multiplayer/Multiplayer.cs | 6 +++++ .../Screens/OnlinePlay/OnlinePlayScreen.cs | 13 ++++++----- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c60bff9e4c..5372f9227f 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -46,6 +46,7 @@ using osu.Game.IO; using osu.Game.Localisation; using osu.Game.Online; using osu.Game.Online.Chat; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.Music; @@ -58,6 +59,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; +using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; @@ -643,6 +645,25 @@ namespace osu.Game }); } + /// + /// Join a multiplayer match immediately. + /// + /// The room to join. + /// The password to join the room, if any is given. + public void PresentMultiplayerMatch(Room room, string password) + { + PerformFromScreen(screen => + { + Multiplayer multiplayer = new Multiplayer(); + multiplayer.OnLoadComplete += _ => + { + multiplayer.Join(room, password); + }; + + screen.Push(multiplayer); + }); + } + /// /// Present a score's replay immediately. /// The user should have already requested this interactively. @@ -853,6 +874,7 @@ namespace osu.Game ScoreManager.PresentImport = items => PresentScore(items.First().Value); MultiplayerClient.PostNotification = n => Notifications.Post(n); + MultiplayerClient.InviteAccepted = PresentMultiplayerMatch; // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 514b80b999..f74b5dd96e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Lounge; @@ -90,6 +91,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); + public void Join(Room room, string? password) + { + LoungeSubScreen.Join(room, password); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 37b50b4863..85eec7ac07 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -32,8 +32,9 @@ namespace osu.Game.Screens.OnlinePlay // while leases may be taken out by a subscreen. public override bool DisallowExternalBeatmapRulesetChanges => true; + protected LoungeSubScreen LoungeSubScreen; + private MultiplayerWaveContainer waves; - private LoungeSubScreen loungeSubScreen; private ScreenStack screenStack; [Cached(Type = typeof(IRoomManager))] @@ -89,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay screenStack.ScreenPushed += screenPushed; screenStack.ScreenExited += screenExited; - screenStack.Push(loungeSubScreen = CreateLounge()); + screenStack.Push(LoungeSubScreen = CreateLounge()); apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); @@ -120,10 +121,10 @@ namespace osu.Game.Screens.OnlinePlay Mods.SetDefault(); - if (loungeSubScreen.IsCurrentScreen()) - loungeSubScreen.OnEntering(e); + if (LoungeSubScreen.IsCurrentScreen()) + LoungeSubScreen.OnEntering(e); else - loungeSubScreen.MakeCurrent(); + LoungeSubScreen.MakeCurrent(); } public override void OnResuming(ScreenTransitionEvent e) @@ -158,7 +159,7 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnExiting(ScreenExitEvent e) { - while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not LoungeSubScreen) + while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not Lounge.LoungeSubScreen) { var subScreen = (Screen)screenStack.CurrentScreen; if (subScreen.IsLoaded && subScreen.OnExiting(e)) From 00867593950322e5dbf46b1756a29eb5ea7c71bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 15:23:22 +0900 Subject: [PATCH 032/896] Fix slider tracking state being set to `false` after slider end --- .../Objects/Drawables/DrawableSliderBall.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 47214f1e53..63e500d687 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -132,6 +132,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); + if (Time.Current >= drawableSlider.HitObject.EndTime) + return; + // from the point at which the head circle is hit, this will be non-null. // it may be null if the head circle was missed. var headCircleHitAction = GetInitialHitAction(); @@ -153,9 +156,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking = // in valid time range - Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime && + Time.Current >= drawableSlider.HitObject.StartTime // in valid position range - lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && + && lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && // valid action (actions?.Any(isValidTrackingAction) ?? false); From 9a3c21c3206a9853e5379783f83c38e9439f2cb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Oct 2023 15:30:33 +0900 Subject: [PATCH 033/896] Change comparison of `timeOffset` to greater-than (in line with others) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 7daab820dd..9edf3808c5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // An actual tick miss should only occur if reaching the tick itself. if (timeOffset >= SliderEventGenerator.TAIL_LENIENCY && Tracking) ApplyResult(r => r.Type = r.Judgement.MaxResult); - else if (timeOffset >= 0) + else if (timeOffset > 0) ApplyResult(r => r.Type = r.Judgement.MinResult); } From 941f26d46233902f88a4088c3bfa354064e13dee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Oct 2023 18:35:01 +0900 Subject: [PATCH 034/896] Add extra test coverage to `TestSceneOsuModAutoplay` to cover fail case Basically the slider needs to be slightly longer for this test to correctly fail in headless tests, in conjunction with the new slider tail leniency. This is due to headless tests running at a fixed frame interval, and these timings being *tight*. --- .../Mods/TestSceneOsuModAutoplay.cs | 7 ++++--- .../TestSceneSliderInput.cs | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs index 6980438b59..ace7f23989 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -51,8 +51,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods FinalRate = { Value = 1.3 } }); - [Test] - public void TestPerfectScoreOnShortSliderWithRepeat() + [TestCase(6.25f)] + [TestCase(20)] + public void TestPerfectScoreOnShortSliderWithRepeat(float pathLength) { AddStep("set score to standardised", () => LocalConfig.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised)); @@ -70,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Path = new SliderPath(new[] { new PathControlPoint(), - new PathControlPoint(new Vector2(0, 6.25f)) + new PathControlPoint(new Vector2(0, pathLength)) }), RepeatCount = 1, SliderVelocityMultiplier = 10 diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index e98e7f45f6..9e83ca428b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.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 NUnit.Framework; @@ -42,9 +41,17 @@ namespace osu.Game.Rulesets.Osu.Tests // Making these too short causes breakage from frames not being processed fast enough. // To keep things simple, these tests are crafted to always be >16ms length. // If sliders shorter than this are ever used in gameplay it will probably break things and we can revisit. - [TestCase(80, 0)] + [TestCase(30, 0)] + [TestCase(30, 1)] + [TestCase(40, 0)] + [TestCase(40, 1)] + [TestCase(50, 1)] + [TestCase(60, 1)] + [TestCase(70, 1)] [TestCase(80, 1)] + [TestCase(80, 0)] [TestCase(80, 10)] + [TestCase(90, 1)] public void TestVeryShortSlider(float sliderLength, int repeatCount) { Slider slider; @@ -66,15 +73,15 @@ namespace osu.Game.Rulesets.Osu.Tests }), }, 240, 1); - AddAssert("Slider is longer than one frame", () => slider.Duration / (slider.RepeatCount + 1), () => Is.GreaterThan(1000 / 60f)); - AddAssert("Slider is shorter than lenience", () => slider.Duration / (slider.RepeatCount + 1), () => Is.LessThan(Math.Abs(SliderEventGenerator.TAIL_LENIENCY))); - assertAllMaxJudgements(); // Even if the last tick is hit early, the slider should always execute its final judgement at its endtime. // If not, hitsounds will not play on time. AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0); AddAssert("Slider judged at end time", () => judgementResults.Last().TimeAbsolute, () => Is.EqualTo(slider.EndTime)); + + AddAssert("Slider is last judgement", () => judgementResults[^1].HitObject, Is.TypeOf); + AddAssert("Tail is second last judgement", () => judgementResults[^2].HitObject, Is.TypeOf); } [TestCase(300, false)] From 9e1fec0213d8541a813300d415d1956417979c6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Oct 2023 18:36:13 +0900 Subject: [PATCH 035/896] Fix potential out-of-order tail and ticks --- .../Objects/Drawables/DrawableSliderTail.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 9edf3808c5..df55f3e73f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -129,6 +129,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered) return; + // Ensure the tail can only activate after all previous ticks already have. + // + // This covers the edge case where the lenience may allow the tail to activate before + // the last tick, changing ordering of score/combo awarding. + if (DrawableSlider.NestedHitObjects.Count > 1 && !DrawableSlider.NestedHitObjects[^2].Judged) + return; + // The player needs to have engaged in tracking at any point after the tail leniency cutoff. // An actual tick miss should only occur if reaching the tick itself. if (timeOffset >= SliderEventGenerator.TAIL_LENIENCY && Tracking) From 70ec4b060af1abd4752e483cacbab07b9c755692 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Oct 2023 19:20:37 +0900 Subject: [PATCH 036/896] Rename weird diffcalc parameter name --- .../Difficulty/Evaluators/AimEvaluator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs index cf35b08822..3d1939acac 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators /// and slider difficulty. /// /// - public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliders) + public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance) { if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner) return 0; @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime; // But if the last object is a slider, then we extend the travel velocity through the slider into the current object. - if (osuLastObj.BaseObject is Slider && withSliders) + if (osuLastObj.BaseObject is Slider && withSliderTravelDistance) { double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end. double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators // As above, do the same for the previous hitobject. double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime; - if (osuLastLastObj.BaseObject is Slider && withSliders) + if (osuLastLastObj.BaseObject is Slider && withSliderTravelDistance) { double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime; double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime; @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier); // Add in additional slider velocity bonus. - if (withSliders) + if (withSliderTravelDistance) aimStrain += sliderBonus * slider_multiplier; return aimStrain; From c4992d347975459823ee14ef3c07e42010470b98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Oct 2023 19:08:59 +0900 Subject: [PATCH 037/896] Fix one case of difficulty calculation no longer accounting for leniency --- .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index e627c9ad67..00cb4acfb4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (slider.LazyEndPosition != null) return; - slider.LazyTravelTime = slider.NestedHitObjects[^1].StartTime - slider.StartTime; + slider.LazyTravelTime = slider.NestedHitObjects[^1].StartTime + SliderEventGenerator.TAIL_LENIENCY - slider.StartTime; double endTimeMin = slider.LazyTravelTime / slider.SpanDuration; if (endTimeMin % 2 >= 1) From 267d1ee7d442ef09bbdd5b7eb285c264d424ffb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Tue, 3 Oct 2023 22:08:14 +0200 Subject: [PATCH 038/896] Handle cases when player cannot be invited. --- .../Multiplayer/IMultiplayerRoomServer.cs | 3 +- .../Multiplayer/OnlineMultiplayerClient.cs | 29 +++++++++++++++++-- .../Multiplayer/UserBlockedException.cs | 26 +++++++++++++++++ .../Multiplayer/UserBlocksPMsException.cs | 26 +++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/UserBlockedException.cs create mode 100644 osu.Game/Online/Multiplayer/UserBlocksPMsException.cs diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 64cd6df24d..e98570dc29 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -104,7 +104,8 @@ namespace osu.Game.Online.Multiplayer /// Invites a player to the current room. /// /// - /// + /// The user has blocked or has been blocked by the invited user. + /// The invited user does not accept private messages. Task InvitePlayer(int userId); } } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 5b5741ef1c..08e82f2ad3 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Notifications; namespace osu.Game.Online.Multiplayer { @@ -107,14 +108,36 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } - public override Task InvitePlayer(int userId) + public override async Task InvitePlayer(int userId) { if (!IsConnected.Value) - return Task.CompletedTask; + return; Debug.Assert(connection != null); - return connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId); + try + { + await connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId).ConfigureAwait(false); + } + catch (HubException exception) + { + switch (exception.GetHubExceptionMessage()) + { + case UserBlockedException.MESSAGE: + PostNotification?.Invoke(new SimpleErrorNotification + { + Text = "User cannot be invited by someone they have blocked or are blocked by." + }); + break; + + case UserBlocksPMsException.MESSAGE: + PostNotification?.Invoke(new SimpleErrorNotification + { + Text = "User cannot be invited because they cannot receive private messages from people not on their friends list." + }); + break; + } + } } public override Task TransferHost(int userId) diff --git a/osu.Game/Online/Multiplayer/UserBlockedException.cs b/osu.Game/Online/Multiplayer/UserBlockedException.cs new file mode 100644 index 0000000000..363f878183 --- /dev/null +++ b/osu.Game/Online/Multiplayer/UserBlockedException.cs @@ -0,0 +1,26 @@ +// 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.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; + +namespace osu.Game.Online.Multiplayer +{ + [Serializable] + public class UserBlockedException : HubException + { + public const string MESSAGE = "User cannot be invited by someone they have blocked or are blocked by."; + + public UserBlockedException() + : + base(MESSAGE) + { + } + + protected UserBlockedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs new file mode 100644 index 0000000000..220a84cfe8 --- /dev/null +++ b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs @@ -0,0 +1,26 @@ +// 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.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; + +namespace osu.Game.Online.Multiplayer +{ + [Serializable] + public class UserBlocksPMsException : HubException + { + public const string MESSAGE = "User cannot be invited because they have disabled private messages."; + + public UserBlocksPMsException() + : + base(MESSAGE) + { + } + + protected UserBlocksPMsException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} From 32f69cd0ba58e9433214862751e830d33e70e748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 00:20:07 +0200 Subject: [PATCH 039/896] Make `UserAvatarNotification.user` readonly --- osu.Game/Overlays/Notifications/UserAvatarNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs index 191d63a76f..8f32c9d395 100644 --- a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Notifications private TextFlowContainer? textDrawable; - private APIUser user; + private readonly APIUser user; public UserAvatarNotification(APIUser user, LocalisableString text) { From 5678d904615f6f300613904363c1ac8d72b26a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 00:20:38 +0200 Subject: [PATCH 040/896] Reduce silliness of notification test case --- .../Visual/UserInterface/TestSceneNotificationOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 07bd722322..332053d71e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -549,7 +549,7 @@ namespace osu.Game.Tests.Visual.UserInterface var user = await userLookupCache.GetUserAsync(0).ConfigureAwait(true); if (user == null) return; - var n = new UserAvatarNotification(user, $"{user.Username} is telling you to NOT download Haitai!"); + var n = new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!"); notificationOverlay.Post(n); } From 5469d134cb0919b70fc6168d24d948d3133b67ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 00:28:01 +0200 Subject: [PATCH 041/896] Add missing parameter description to docs. --- osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index e98570dc29..b7a608581c 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -103,7 +103,7 @@ namespace osu.Game.Online.Multiplayer /// /// Invites a player to the current room. /// - /// + /// The user to invite. /// The user has blocked or has been blocked by the invited user. /// The invited user does not accept private messages. Task InvitePlayer(int userId); From fe5177fa4fe29ea35b3fac74f50b330899dd7066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 00:50:48 +0200 Subject: [PATCH 042/896] Remove unused import --- osu.Game/Overlays/Notifications/UserAvatarNotification.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs index 8f32c9d395..af881a5008 100644 --- a/osu.Game/Overlays/Notifications/UserAvatarNotification.cs +++ b/osu.Game/Overlays/Notifications/UserAvatarNotification.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; From 8d919912149a4853120612b67944492c27baa611 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Oct 2023 20:32:46 +0900 Subject: [PATCH 043/896] Fix difficulty calculation not correct handling slider leniency anymore --- .../Preprocessing/OsuDifficultyHitObject.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 00cb4acfb4..5c2c85d69e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Mods; @@ -214,7 +215,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (slider.LazyEndPosition != null) return; - slider.LazyTravelTime = slider.NestedHitObjects[^1].StartTime + SliderEventGenerator.TAIL_LENIENCY - slider.StartTime; + double trackingEndTime = Math.Max( + // SliderTailCircle always occurs at the final end time of the slider, but the player only needs to hold until within a lenience before it. + slider.NestedHitObjects.OfType().Single().StartTime + SliderEventGenerator.TAIL_LENIENCY, + // There's an edge case where one or more ticks/repeats fall within that leniency range. + // In such a case, the player needs to track until the final tick or repeat. + slider.NestedHitObjects.LastOrDefault(n => n is not SliderTailCircle)?.StartTime ?? double.MinValue + ); + + slider.LazyTravelTime = trackingEndTime - slider.StartTime; double endTimeMin = slider.LazyTravelTime / slider.SpanDuration; if (endTimeMin % 2 >= 1) From 9361b1879c5e15667548b6d3c302c3bb43232d6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Oct 2023 12:54:13 +0900 Subject: [PATCH 044/896] Add note about early return in `DrawableSliderBall.Update` --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 63e500d687..2fa2d4c0b5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -132,6 +132,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); + // even in an edge case where current time has exceeded the slider's time, we may not have finished judging. + // we don't want to potentially update from Tracking=true to Tracking=false at this point. if (Time.Current >= drawableSlider.HitObject.EndTime) return; From cfb18e18c060f4270925b544fa545518ac48dea6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Oct 2023 13:33:06 +0900 Subject: [PATCH 045/896] Add better commenting around legacy last tick edge case --- .../Rulesets/Objects/SliderEventGenerator.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index 42c422a637..9b8375f208 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -88,18 +88,27 @@ namespace osu.Game.Rulesets.Objects int finalSpanIndex = spanCount - 1; double finalSpanStartTime = startTime + finalSpanIndex * spanDuration; - double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) + TAIL_LENIENCY); - double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration; - if (spanCount % 2 == 0) finalProgress = 1 - finalProgress; + // Note that `finalSpanStartTime + spanDuration ≈ startTime + totalDuration`, but we write it like this to match floating point precision + // of stable. + // + // So thinking about this in a saner way, the time of the LegacyLastTick is + // + // `slider.StartTime + max(slider.Duration / 2, slider.Duration - 36)` + // + // As a slider gets shorter than 72 ms, the leniency offered falls below the 36 ms `TAIL_LENIENCY` constant. + double legacyLastTickTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) + TAIL_LENIENCY); + double legacyLastTickProgress = (legacyLastTickTime - finalSpanStartTime) / spanDuration; + + if (spanCount % 2 == 0) legacyLastTickProgress = 1 - legacyLastTickProgress; yield return new SliderEventDescriptor { Type = SliderEventType.LegacyLastTick, SpanIndex = finalSpanIndex, SpanStartTime = finalSpanStartTime, - Time = finalSpanEndTime, - PathProgress = finalProgress, + Time = legacyLastTickTime, + PathProgress = legacyLastTickProgress, }; yield return new SliderEventDescriptor From d14d885d19a018343f9aa81ca7f67e84da917865 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Oct 2023 14:52:35 +0900 Subject: [PATCH 046/896] Add test coverage of very fast slider --- .../OsuDifficultyCalculatorTest.cs | 3 +++ .../Testing/Beatmaps/very-fast-slider.osu | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/very-fast-slider.osu diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 9c6449cfa9..f1c1c4734d 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -17,16 +17,19 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(6.7115569159190587d, 206, "diffcalc-test")] [TestCase(1.4391311903612753d, 45, "zero-length-sliders")] + [TestCase(0.42506480230838789d, 2, "very-fast-slider")] [TestCase(0.14102693012101306d, 1, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); [TestCase(8.9757300665532966d, 206, "diffcalc-test")] + [TestCase(0.55071082800473514d, 2, "very-fast-slider")] [TestCase(1.7437232654020756d, 45, "zero-length-sliders")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); [TestCase(6.7115569159190587d, 239, "diffcalc-test")] + [TestCase(0.42506480230838789d, 4, "very-fast-slider")] [TestCase(1.4391311903612753d, 54, "zero-length-sliders")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/very-fast-slider.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/very-fast-slider.osu new file mode 100644 index 0000000000..58ef36e70c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/very-fast-slider.osu @@ -0,0 +1,21 @@ +osu file format v128 + +[Difficulty] +HPDrainRate: 3 +CircleSize: 4 +OverallDifficulty: 9 +ApproachRate: 9.3 +SliderMultiplier: 3.59999990463257 +SliderTickRate: 1 + +[TimingPoints] +812,342.857142857143,4,1,1,70,1,0 +57383,-28.5714285714286,4,1,1,70,0,0 + +[HitObjects] +// Taken from https://osu.ppy.sh/beatmapsets/881996#osu/1844019 +// This slider is 42 ms in length, triggering the LegacyLastTick edge case. +// The tick will be at 21.5 ms (sliderDuration / 2) instead of 6 ms (sliderDuration - LAST_TICK_LENIENCE). +416,41,57383,6,0,L|467:217,1,157.499997329712,2|0,3:3|3:0,3:0:0:0: +// Include the next slider as well to cover the jump back to the start position. +407,73,57469,2,0,L|470:215,1,129.599999730835,2|0,0:0|0:0,0:0:0:0: From bfeafd6f70bcd0832c144b3bf8d7cbd75c148285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:30:48 +0200 Subject: [PATCH 047/896] Fix formatting --- osu.Game/Online/Multiplayer/UserBlockedException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/UserBlockedException.cs b/osu.Game/Online/Multiplayer/UserBlockedException.cs index 363f878183..f2b69411c7 100644 --- a/osu.Game/Online/Multiplayer/UserBlockedException.cs +++ b/osu.Game/Online/Multiplayer/UserBlockedException.cs @@ -13,8 +13,7 @@ namespace osu.Game.Online.Multiplayer public const string MESSAGE = "User cannot be invited by someone they have blocked or are blocked by."; public UserBlockedException() - : - base(MESSAGE) + : base(MESSAGE) { } From 74ed3bc4ff8f8206188ec115c319222b0b31a99c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:31:50 +0200 Subject: [PATCH 048/896] Rename `loadRoom` to `lookupRoom` --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 61c2fa495a..8b4c38a152 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -448,7 +448,7 @@ namespace osu.Game.Online.Multiplayer async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password) { var loadUserTask = userLookupCache.GetUserAsync(invitedBy); - var loadRoomTask = loadRoom(roomID); + var loadRoomTask = lookupRoom(roomID); await Task.WhenAll(loadUserTask, loadRoomTask).ConfigureAwait(false); @@ -472,7 +472,7 @@ namespace osu.Game.Online.Multiplayer }); } - private Task loadRoom(long id) + private Task lookupRoom(long id) { return Task.Run(() => { From 0726ccb9887a9be8c653cf61de6fbf0a4c31bbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:32:18 +0200 Subject: [PATCH 049/896] Fix formatting --- osu.Game/Online/Multiplayer/UserBlocksPMsException.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs index 220a84cfe8..0fd4e1f0c1 100644 --- a/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs +++ b/osu.Game/Online/Multiplayer/UserBlocksPMsException.cs @@ -13,8 +13,7 @@ namespace osu.Game.Online.Multiplayer public const string MESSAGE = "User cannot be invited because they have disabled private messages."; public UserBlocksPMsException() - : - base(MESSAGE) + : base(MESSAGE) { } From 6bd51b32b47919a69a096c09ff8eac76c8bbe477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:35:45 +0200 Subject: [PATCH 050/896] Make resolved `multiplayerClient` field nullable --- osu.Game/Users/UserPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 29a6c65555..98b306e264 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -64,7 +64,7 @@ namespace osu.Game.Users protected OsuColour Colours { get; private set; } = null!; [Resolved] - private MultiplayerClient multiplayerClient { get; set; } = null!; + private MultiplayerClient? multiplayerClient { get; set; } [BackgroundDependencyLoader] private void load() @@ -124,7 +124,7 @@ namespace osu.Game.Users if ( User.IsOnline && - multiplayerClient.Room != null && + multiplayerClient?.Room != null && multiplayerClient.Room.Users.All(u => u.UserID != User.Id) ) { From 6d97f89399ec328b178f8f0561d0479650f2c86c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Wed, 4 Oct 2023 08:37:22 +0200 Subject: [PATCH 051/896] Rename `OnlinePlayScreen.LoungeSubScreen` to `Lounge` --- .../Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index f74b5dd96e..f899b24d30 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public void Join(Room room, string? password) { - LoungeSubScreen.Join(room, password); + Lounge.Join(room, password); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 85eec7ac07..57e388f016 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay // while leases may be taken out by a subscreen. public override bool DisallowExternalBeatmapRulesetChanges => true; - protected LoungeSubScreen LoungeSubScreen; + protected LoungeSubScreen Lounge; private MultiplayerWaveContainer waves; private ScreenStack screenStack; @@ -90,7 +90,7 @@ namespace osu.Game.Screens.OnlinePlay screenStack.ScreenPushed += screenPushed; screenStack.ScreenExited += screenExited; - screenStack.Push(LoungeSubScreen = CreateLounge()); + screenStack.Push(Lounge = CreateLounge()); apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); @@ -121,10 +121,10 @@ namespace osu.Game.Screens.OnlinePlay Mods.SetDefault(); - if (LoungeSubScreen.IsCurrentScreen()) - LoungeSubScreen.OnEntering(e); + if (Lounge.IsCurrentScreen()) + Lounge.OnEntering(e); else - LoungeSubScreen.MakeCurrent(); + Lounge.MakeCurrent(); } public override void OnResuming(ScreenTransitionEvent e) @@ -159,7 +159,7 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnExiting(ScreenExitEvent e) { - while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not Lounge.LoungeSubScreen) + while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not LoungeSubScreen) { var subScreen = (Screen)screenStack.CurrentScreen; if (subScreen.IsLoaded && subScreen.OnExiting(e)) From 658b6a166f711fd719f05b2d3f746cd33ca8e944 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Oct 2023 16:11:39 +0900 Subject: [PATCH 052/896] Fix regression in `Tracking` state handling It turns out multiple components depend on `Tracking` eventually becoming `false` at the end of a slider. With my previous changes, this was no longer the case (as could be seen by the legacy cursor particles test failure, and heard by slider slide taking too long to stop). --- .../Objects/Drawables/DrawableSliderBall.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs index 2fa2d4c0b5..292f2ffd7d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Skinning.Default; @@ -132,11 +133,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); - // even in an edge case where current time has exceeded the slider's time, we may not have finished judging. - // we don't want to potentially update from Tracking=true to Tracking=false at this point. - if (Time.Current >= drawableSlider.HitObject.EndTime) - return; - // from the point at which the head circle is hit, this will be non-null. // it may be null if the head circle was missed. var headCircleHitAction = GetInitialHitAction(); @@ -159,6 +155,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking = // in valid time range Time.Current >= drawableSlider.HitObject.StartTime + // even in an edge case where current time has exceeded the slider's time, we may not have finished judging. + // we don't want to potentially update from Tracking=true to Tracking=false at this point. + && (!drawableSlider.AllJudged || Time.Current <= drawableSlider.HitObject.GetEndTime()) // in valid position range && lastScreenSpaceMousePosition.HasValue && followCircleReceptor.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && // valid action From b2d9c95441fb6679572d036344559fd8657f1946 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Oct 2023 08:17:27 +0900 Subject: [PATCH 053/896] Remove HoldNoteTickJudgement --- .../Judgements/HoldNoteTickJudgement.cs | 12 ------------ .../Skinning/Legacy/LegacyHitExplosion.cs | 3 --- osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs | 3 --- 3 files changed, 18 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs deleted file mode 100644 index ae9e8bd287..0000000000 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ /dev/null @@ -1,12 +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.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Mania.Judgements -{ - public class HoldNoteTickJudgement : ManiaJudgement - { - public override HitResult MaxResult => HitResult.LargeTickHit; - } -} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs index 6c56db613c..43489cf325 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs @@ -69,9 +69,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy public void Animate(JudgementResult result) { - if (result.Judgement is HoldNoteTickJudgement) - return; - (explosion as IFramedAnimation)?.GotoFrame(0); explosion?.FadeInFromZero(FADE_IN_DURATION) diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index e0663e9878..223afafeb6 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -150,9 +150,6 @@ namespace osu.Game.Rulesets.Mania.UI // scale roughly in-line with visual appearance of notes Vector2 scale = new Vector2(1, 0.6f); - if (result.Judgement is HoldNoteTickJudgement) - scale *= 0.5f; - this.ScaleTo(scale); largeFaint From 9415fe4446cd586348534bc8ac7f37dde3d6fda7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Oct 2023 09:47:00 +0900 Subject: [PATCH 054/896] Add mania hold note body + combo break judgement --- .../Skinning/TestSceneHitExplosion.cs | 2 +- .../Judgements/HoldNoteHoldJudgement.cs | 13 +++ .../Objects/Drawables/DrawableHoldNote.cs | 29 ++--- .../Objects/Drawables/DrawableHoldNoteBody.cs | 30 +++++ .../Objects/Drawables/DrawableHoldNoteTail.cs | 4 +- osu.Game.Rulesets.Mania/Objects/HeadNote.cs | 3 + osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 12 ++ .../Objects/HoldNoteBody.cs | 15 +++ osu.Game.Rulesets.Mania/Objects/TailNote.cs | 3 + .../Skinning/Legacy/LegacyBodyPiece.cs | 3 +- osu.Game.Rulesets.Mania/UI/Column.cs | 1 + osu.Game/Rulesets/Judgements/Judgement.cs | 39 ++++++- osu.Game/Rulesets/Scoring/HitResult.cs | 109 ++++++++++++++++++ 13 files changed, 246 insertions(+), 17 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Judgements/HoldNoteHoldJudgement.cs create mode 100644 osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs create mode 100644 osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index 483c468c1e..a0833ff91f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { c.Add(hitExplosionPools[poolIndex].Get(e => { - e.Apply(new JudgementResult(new HitObject(), runCount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement())); + e.Apply(new JudgementResult(new HitObject(), new ManiaJudgement())); e.Anchor = Anchor.Centre; e.Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteHoldJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteHoldJudgement.cs new file mode 100644 index 0000000000..0dbdd4afe6 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteHoldJudgement.cs @@ -0,0 +1,13 @@ +// 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.Scoring; + +namespace osu.Game.Rulesets.Mania.Judgements +{ + public class HoldNoteHoldJudgement : ManiaJudgement + { + public override HitResult MaxResult => HitResult.IgnoreHit; + public override HitResult MinResult => HitResult.ComboBreak; + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3032a3ee39..499f8c99b0 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -35,9 +35,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public DrawableHoldNoteHead Head => headContainer.Child; public DrawableHoldNoteTail Tail => tailContainer.Child; + public DrawableHoldNoteBody Body => bodyContainer.Child; private Container headContainer; private Container tailContainer; + private Container bodyContainer; private PausableSkinnableSound slidingSample; @@ -58,11 +60,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public double? HoldStartTime { get; private set; } - /// - /// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score. - /// - public double? HoldBrokenTime { get; private set; } - /// /// Whether the hold note has been released potentially without having caused a break. /// @@ -102,6 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables headContainer = new Container { RelativeSizeAxes = Axes.Both } } }, + bodyContainer = new Container { RelativeSizeAxes = Axes.Both }, bodyPiece = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece { RelativeSizeAxes = Axes.Both, @@ -133,7 +131,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables sizingContainer.Size = Vector2.One; HoldStartTime = null; - HoldBrokenTime = null; releaseTime = null; } @@ -150,6 +147,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables case DrawableHoldNoteTail tail: tailContainer.Child = tail; break; + + case DrawableHoldNoteBody body: + bodyContainer.Child = body; + break; } } @@ -158,6 +159,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.ClearNestedHitObjects(); headContainer.Clear(false); tailContainer.Clear(false); + bodyContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -169,6 +171,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables case HeadNote head: return new DrawableHoldNoteHead(head); + + case HoldNoteBody body: + return new DrawableHoldNoteBody(body); } return base.CreateNestedHitObject(hitObject); @@ -261,8 +266,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables MissForcefully(); } - if (Tail.Judged && !Tail.IsHit) - HoldBrokenTime = Time.Current; + // Make sure that the hold note is fully judged by giving the body a judgement. + if (Tail.AllJudged && !Body.AllJudged) + Body.TriggerResult(Tail.IsHit); } public override void MissForcefully() @@ -325,12 +331,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return; Tail.UpdateResult(); + Body.TriggerResult(Tail.IsHit); + endHold(); - - // If the key has been released too early, the user should not receive full score for the release - if (!Tail.IsHit) - HoldBrokenTime = Time.Current; - releaseTime = Time.Current; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs new file mode 100644 index 0000000000..ded88af145 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + public partial class DrawableHoldNoteBody : DrawableManiaHitObject + { + public bool HasHoldBreak => AllJudged && !IsHit; + + public override bool DisplayResult => false; + + public DrawableHoldNoteBody() + : this(null) + { + } + + public DrawableHoldNoteBody(HoldNoteBody hitObject) + : base(hitObject) + { + } + + internal void TriggerResult(bool hit) + { + if (!AllJudged) + ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index e7326df07d..a559e91f1b 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -55,7 +55,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables ApplyResult(r => { // If the head wasn't hit or the hold note was broken, cap the max score to Meh. - if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null)) + bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak; + + if (result > HitResult.Meh && hasComboBreak) result = HitResult.Meh; r.Type = result; diff --git a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs index e69cc62aed..a2e89ea560 100644 --- a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs @@ -3,6 +3,9 @@ namespace osu.Game.Rulesets.Mania.Objects { + /// + /// The head note of a . + /// public class HeadNote : Note { } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 333912dd0c..3f930a310b 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -79,6 +79,12 @@ namespace osu.Game.Rulesets.Mania.Objects /// public TailNote Tail { get; private set; } + /// + /// The body of the hold. + /// This is an invisible and silent object that tracks the holding state of the . + /// + public HoldNoteBody Body { get; private set; } + public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset; protected override void CreateNestedHitObjects(CancellationToken cancellationToken) @@ -98,6 +104,12 @@ namespace osu.Game.Rulesets.Mania.Objects Column = Column, Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1), }); + + AddNested(Body = new HoldNoteBody + { + StartTime = StartTime, + Column = Column + }); } public override Judgement CreateJudgement() => new IgnoreJudgement(); diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs new file mode 100644 index 0000000000..ea0643fbb8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs @@ -0,0 +1,15 @@ +// 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.Rulesets.Mania.Objects +{ + /// + /// The body of a . + /// Mostly a dummy hitobject that provides the judgement for the "holding" state.
+ /// On hit - the hold note was held correctly for the full duration.
+ /// On miss - the hold note was released at some point during its judgement period. + ///
+ public class HoldNoteBody : ManiaHitObject + { + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/TailNote.cs b/osu.Game.Rulesets.Mania/Objects/TailNote.cs index 71a594c6ce..def32880f1 100644 --- a/osu.Game.Rulesets.Mania/Objects/TailNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/TailNote.cs @@ -6,6 +6,9 @@ using osu.Game.Rulesets.Mania.Judgements; namespace osu.Game.Rulesets.Mania.Objects { + /// + /// The tail note of a . + /// public class TailNote : Note { /// diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index ef4810c40d..bd177d5c7a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -209,7 +209,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy protected override void Update() { base.Update(); - missFadeTime.Value ??= holdNote.HoldBrokenTime; + + missFadeTime.Value = holdNote.Body.HasHoldBreak ? holdNote.Body.Result.TimeAbsolute : null; int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1); diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 1c4f0462ec..6cd55bb099 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -112,6 +112,7 @@ namespace osu.Game.Rulesets.Mania.UI RegisterPool(10, 50); RegisterPool(10, 50); RegisterPool(10, 50); + RegisterPool(10, 50); } private void onSourceChanged() diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 99dce82ec2..3de1b03583 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -35,7 +35,40 @@ namespace osu.Game.Rulesets.Judgements /// /// The minimum that can be achieved - the inverse of . /// - public HitResult MinResult + /// + /// Defaults to a sane value for the given . May be overridden to provide a supported custom value: + /// + /// + /// Valid s + /// Valid s + /// + /// + /// , , , , + /// , , + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// , , + /// + /// + /// + /// , , + /// + /// + /// + /// , + /// + /// + /// + public virtual HitResult MinResult { get { @@ -58,6 +91,10 @@ namespace osu.Game.Rulesets.Judgements } } + public Judgement() + { + } + /// /// The numeric score representation for the maximum achievable result. /// diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 0013a9f20d..6303062f03 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -120,6 +120,16 @@ namespace osu.Game.Rulesets.Scoring [Order(12)] IgnoreHit, + /// + /// Indicates that a combo break should occur, but does not otherwise affect score. + /// + /// + /// May be paired with . + /// + [EnumMember(Value = "combo_break")] + [Order(15)] + ComboBreak, + /// /// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy). /// @@ -291,6 +301,105 @@ namespace osu.Game.Rulesets.Scoring /// The to get the index of. /// The index of . public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result); + + public static void ValidateHitResultPair(HitResult maxResult, HitResult minResult) + { + // Check valid maximum judgements. + switch (maxResult) + { + case HitResult.Meh: + case HitResult.Ok: + case HitResult.Good: + case HitResult.Great: + case HitResult.Perfect: + case HitResult.SmallTickHit: + case HitResult.LargeTickHit: + case HitResult.SmallBonus: + case HitResult.LargeBonus: + case HitResult.IgnoreHit: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result."); + } + + // Check valid minimum judgements. + switch (minResult) + { + case HitResult.Miss: + case HitResult.SmallTickMiss: + case HitResult.LargeTickMiss: + case HitResult.IgnoreMiss: + case HitResult.ComboBreak: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result."); + } + + // Check valid category pairings. + switch (maxResult) + { + case HitResult.SmallBonus: + case HitResult.LargeBonus: + switch (minResult) + { + case HitResult.IgnoreMiss: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement."); + } + + break; + + case HitResult.SmallTickHit: + switch (minResult) + { + case HitResult.SmallTickMiss: + case HitResult.IgnoreMiss: + case HitResult.ComboBreak: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); + } + + break; + + case HitResult.LargeTickHit: + switch (minResult) + { + case HitResult.LargeTickMiss: + case HitResult.IgnoreMiss: + case HitResult.ComboBreak: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); + } + + break; + + case HitResult.Meh: + case HitResult.Ok: + case HitResult.Good: + case HitResult.Great: + case HitResult.Perfect: + switch (minResult) + { + case HitResult.Miss: + case HitResult.IgnoreMiss: + case HitResult.ComboBreak: + break; + + default: + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); + } + + break; + } + } } #pragma warning restore CS0618 } From 65ac617be4403ffb7775c8ec08c1e00eec3d9633 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Oct 2023 10:45:29 +0900 Subject: [PATCH 055/896] Actually validate judgements --- osu.Game/Rulesets/Judgements/Judgement.cs | 4 ---- osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 2 ++ 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 3de1b03583..b9e88dce7a 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -91,10 +91,6 @@ namespace osu.Game.Rulesets.Judgements } } - public Judgement() - { - } - /// /// The numeric score representation for the maximum achievable result. /// diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index e9f3bcb949..bf6c2bfe28 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -151,6 +151,8 @@ namespace osu.Game.Rulesets.Scoring { var judgement = obj.CreateJudgement(); + HitResultExtensions.ValidateHitResultPair(judgement.MaxResult, judgement.MinResult); + var result = CreateResult(obj, judgement); if (result == null) throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); From d988ef9b227184e06f6021cce38ac62a3a72f109 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Oct 2023 10:50:39 +0900 Subject: [PATCH 056/896] Revert functional change --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index bd177d5c7a..660f72e565 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -210,7 +210,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy { base.Update(); - missFadeTime.Value = holdNote.Body.HasHoldBreak ? holdNote.Body.Result.TimeAbsolute : null; + if (holdNote.Body.HasHoldBreak) + missFadeTime.Value = holdNote.Body.Result.TimeAbsolute; int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1); From f0da7f016d74f7002c76e8d8c1b4b208e133a447 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Oct 2023 11:49:54 +0900 Subject: [PATCH 057/896] Actually use the judgement --- .../{HoldNoteHoldJudgement.cs => HoldNoteBodyJudgement.cs} | 2 +- osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) rename osu.Game.Rulesets.Mania/Judgements/{HoldNoteHoldJudgement.cs => HoldNoteBodyJudgement.cs} (87%) diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteHoldJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs similarity index 87% rename from osu.Game.Rulesets.Mania/Judgements/HoldNoteHoldJudgement.cs rename to osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs index 0dbdd4afe6..6719665cbe 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteHoldJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs @@ -5,7 +5,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Judgements { - public class HoldNoteHoldJudgement : ManiaJudgement + public class HoldNoteBodyJudgement : ManiaJudgement { public override HitResult MaxResult => HitResult.IgnoreHit; public override HitResult MinResult => HitResult.ComboBreak; diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs index ea0643fbb8..92baa25fa9 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs @@ -1,6 +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.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; + namespace osu.Game.Rulesets.Mania.Objects { /// @@ -11,5 +14,6 @@ namespace osu.Game.Rulesets.Mania.Objects /// public class HoldNoteBody : ManiaHitObject { + public override Judgement CreateJudgement() => new HoldNoteBodyJudgement(); } } From 326702165f75cfea9b9eb9f110fb9cdfb2eeba3a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Oct 2023 11:50:17 +0900 Subject: [PATCH 058/896] Update HitResult helpers with new judgement --- osu.Game/Rulesets/Scoring/HitResult.cs | 51 ++++++++++++++++++++------ 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 6303062f03..cc528e406f 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -175,6 +175,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.LargeTickHit: case HitResult.LargeTickMiss: case HitResult.LegacyComboIncrease: + case HitResult.ComboBreak: return true; default: @@ -187,11 +188,19 @@ namespace osu.Game.Rulesets.Scoring /// public static bool AffectsAccuracy(this HitResult result) { - // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result. - if (result == HitResult.LegacyComboIncrease) - return false; + switch (result) + { + // LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result. + case HitResult.LegacyComboIncrease: + return false; - return IsScorable(result) && !IsBonus(result); + // ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting. + case HitResult.ComboBreak: + return false; + + default: + return IsScorable(result) && !IsBonus(result); + } } /// @@ -199,11 +208,19 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsBasic(this HitResult result) { - // LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result. - if (result == HitResult.LegacyComboIncrease) - return false; + switch (result) + { + // LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result. + case HitResult.LegacyComboIncrease: + return false; - return IsScorable(result) && !IsTick(result) && !IsBonus(result); + // ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting. + case HitResult.ComboBreak: + return false; + + default: + return IsScorable(result) && !IsTick(result) && !IsBonus(result); + } } /// @@ -252,6 +269,7 @@ namespace osu.Game.Rulesets.Scoring case HitResult.Miss: case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: + case HitResult.ComboBreak: return false; default: @@ -264,11 +282,20 @@ namespace osu.Game.Rulesets.Scoring /// public static bool IsScorable(this HitResult result) { - // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output. - if (result == HitResult.LegacyComboIncrease) - return true; + switch (result) + { + // LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output. + case HitResult.LegacyComboIncrease: + return true; - return result >= HitResult.Miss && result < HitResult.IgnoreMiss; + // ComboBreak is its own type that affects score via combo. + case HitResult.ComboBreak: + return true; + + default: + // Note that IgnoreHit and IgnoreMiss are excluded as they do not affect score. + return result >= HitResult.Miss && result < HitResult.IgnoreMiss; + } } /// From b65c70018f08fefbad1db55eae62cfad413b18d5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Oct 2023 11:58:24 +0900 Subject: [PATCH 059/896] Add test for the result --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 17a4c80f7f..ff87b2a833 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Rulesets.Scoring for (int i = 0; i < 4; i++) { - var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new TestJudgement(maxResult)) + var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], beatmap.HitObjects[i].CreateJudgement()) { Type = i == 2 ? minResult : hitResult }; @@ -259,6 +259,41 @@ namespace osu.Game.Tests.Rulesets.Scoring } #pragma warning restore CS0618 + [Test] + public void TestComboBreak() + { + Assert.That(HitResult.ComboBreak.IncreasesCombo(), Is.False); + Assert.That(HitResult.ComboBreak.BreaksCombo(), Is.True); + Assert.That(HitResult.ComboBreak.AffectsCombo(), Is.True); + Assert.That(HitResult.ComboBreak.AffectsAccuracy(), Is.False); + Assert.That(HitResult.ComboBreak.IsBasic(), Is.False); + Assert.That(HitResult.ComboBreak.IsTick(), Is.False); + Assert.That(HitResult.ComboBreak.IsBonus(), Is.False); + Assert.That(HitResult.ComboBreak.IsHit(), Is.False); + Assert.That(HitResult.ComboBreak.IsScorable(), Is.True); + Assert.That(HitResultExtensions.ALL_TYPES, Does.Contain(HitResult.ComboBreak)); + + beatmap = new TestBeatmap(new RulesetInfo()) + { + HitObjects = new List + { + new TestHitObject(HitResult.Great, HitResult.ComboBreak), + new TestHitObject(HitResult.Great, HitResult.ComboBreak), + } + }; + + scoreProcessor = new TestScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Great }); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1)); + Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1)); + + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.ComboBreak }); + Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0)); + Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1)); + } + [Test] public void TestAccuracyWhenNearPerfect() { @@ -275,7 +310,7 @@ namespace osu.Game.Tests.Rulesets.Scoring for (int i = 0; i < beatmap.HitObjects.Count; i++) { - scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great)) + scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], beatmap.HitObjects[i].CreateJudgement()) { Type = i == 0 ? HitResult.Miss : HitResult.Great }); @@ -293,24 +328,31 @@ namespace osu.Game.Tests.Rulesets.Scoring { public override HitResult MaxResult { get; } - public TestJudgement(HitResult maxResult) + public override HitResult MinResult => minResult ?? base.MinResult; + + private readonly HitResult? minResult; + + public TestJudgement(HitResult maxResult, HitResult? minResult = null) { MaxResult = maxResult; + this.minResult = minResult; } } private class TestHitObject : HitObject { private readonly HitResult maxResult; + private readonly HitResult? minResult; public override Judgement CreateJudgement() { - return new TestJudgement(maxResult); + return new TestJudgement(maxResult, minResult); } - public TestHitObject(HitResult maxResult) + public TestHitObject(HitResult maxResult, HitResult? minResult = null) { this.maxResult = maxResult; + this.minResult = minResult; } } From e874ec81c938a922135275e159e08ab9769a2631 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Oct 2023 12:22:08 +0900 Subject: [PATCH 060/896] Remove unused usings --- osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs | 1 - osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs index 43489cf325..1ec218644c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index 223afafeb6..e588951624 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.UI.Scrolling; using osuTK; From ad6f04cfb0030c32797621c896a3eb4a4e65e62a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Oct 2023 04:02:02 +0900 Subject: [PATCH 061/896] Hotfix approach circle cutoff --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs index 403a14214e..eea6606233 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyApproachCircle.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private DrawableHitObject drawableObject { get; set; } = null!; public LegacyApproachCircle() - : base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS) + : base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS * 2) { } From 0fb7895a52c8b3fd9cd374e34c603afa4b7396ec Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 23 Sep 2023 23:39:12 +0300 Subject: [PATCH 062/896] Adjust catcher origin position to match 1:1 with stable Change catcher origin position logic to 1:1 match stable on legacy skin --- .../Skinning/Argon/ArgonCatcher.cs | 6 ++++- .../Skinning/Default/DefaultCatcher.cs | 11 ++++++++ .../Skinning/Legacy/LegacyCatcher.cs | 27 +++++++++++++++++++ .../UI/SkinnableCatcher.cs | 5 ++-- 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs index 82374085c8..5a788a26fb 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs @@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon [BackgroundDependencyLoader] private void load() { - RelativeSizeAxes = Axes.Both; + Anchor = Anchor.TopCentre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs index 72208b763b..bcd4c73f04 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/DefaultCatcher.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Catch.UI; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Default { @@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default public DefaultCatcher() { + Anchor = Anchor.TopCentre; RelativeSizeAxes = Axes.Both; InternalChild = sprite = new Sprite { @@ -32,6 +34,15 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default }; } + protected override void Update() + { + base.Update(); + + // matches stable's origin position since we're using the same catcher sprite. + // see LegacyCatcher for more information. + OriginPosition = new Vector2(DrawWidth / 2, 16f); + } + [BackgroundDependencyLoader] private void load(TextureStore store, Bindable currentState) { diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs new file mode 100644 index 0000000000..cba2c671b0 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.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.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Skinning.Legacy +{ + public abstract partial class LegacyCatcher : CompositeDrawable + { + protected LegacyCatcher() + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + RelativeSizeAxes = Axes.Both; + } + + protected override void Update() + { + base.Update(); + + // stable sets the Y origin position of the catcher to 16px in order for the catching range and OD scaling to align with the top of the catcher's plate in the default skin. + OriginPosition = new Vector2(DrawWidth / 2, 16f); + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs index bcc59a5e4f..107b79c88e 100644 --- a/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs +++ b/osu.Game.Rulesets.Catch/UI/SkinnableCatcher.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Catch.UI { @@ -26,8 +25,8 @@ namespace osu.Game.Rulesets.Catch.UI : base(new CatchSkinComponentLookup(CatchSkinComponents.Catcher), _ => new DefaultCatcher()) { Anchor = Anchor.TopCentre; - // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. - OriginPosition = new Vector2(0.5f, 0.06f) * Catcher.BASE_SIZE; + Origin = Anchor.TopCentre; + CentreComponent = false; } } } From fc63ee43be2a75d39f16606e85f53e1135022027 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 23 Sep 2023 23:42:13 +0300 Subject: [PATCH 063/896] Fix legacy catcher sprites getting shrunk --- .../Skinning/Legacy/LegacyCatcher.cs | 8 +++++++- .../Skinning/Legacy/LegacyCatcherNew.cs | 19 ++----------------- .../Skinning/Legacy/LegacyCatcherOld.cs | 15 +++------------ 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs index cba2c671b0..6cd8b14191 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs @@ -13,7 +13,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; - RelativeSizeAxes = Axes.Both; + + // in stable, catcher sprites are displayed in their raw size. stable also has catcher sprites displayed with the following scale factors applied: + // 1. 0.5x, affecting all sprites in the playfield, computed here based on lazer's catch playfield dimensions (see WIDTH/HEIGHT constants in CatchPlayfield), + // source: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjectManager.cs#L483-L494 + // 2. 0.7x, a constant scale applied to all catcher sprites on construction. + AutoSizeAxes = Axes.Both; + Scale = new Vector2(0.5f * 0.7f); } protected override void Update() diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs index f6b2c52498..54d555b22a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherNew.cs @@ -7,14 +7,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { - public partial class LegacyCatcherNew : CompositeDrawable + public partial class LegacyCatcherNew : LegacyCatcher { [Resolved] private Bindable currentState { get; set; } = null!; @@ -23,25 +21,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy private Drawable currentDrawable = null!; - public LegacyCatcherNew() - { - RelativeSizeAxes = Axes.Both; - } - [BackgroundDependencyLoader] private void load(ISkinSource skin) { foreach (var state in Enum.GetValues()) { - AddInternal(drawables[state] = getDrawableFor(state).With(d => - { - d.Anchor = Anchor.TopCentre; - d.Origin = Anchor.TopCentre; - d.RelativeSizeAxes = Axes.Both; - d.Size = Vector2.One; - d.FillMode = FillMode.Fit; - d.Alpha = 0; - })); + AddInternal(drawables[state] = getDrawableFor(state).With(d => d.Alpha = 0)); } currentDrawable = drawables[CatcherAnimationState.Idle]; diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs index 1e21d8eab1..012200eedf 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcherOld.cs @@ -3,30 +3,21 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Catch.Skinning.Legacy { - public partial class LegacyCatcherOld : CompositeDrawable + public partial class LegacyCatcherOld : LegacyCatcher { public LegacyCatcherOld() { - RelativeSizeAxes = Axes.Both; + AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(ISkinSource skin) { - InternalChild = (skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty()).With(d => - { - d.Anchor = Anchor.TopCentre; - d.Origin = Anchor.TopCentre; - d.RelativeSizeAxes = Axes.Both; - d.Size = Vector2.One; - d.FillMode = FillMode.Fit; - }); + InternalChild = skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty(); } } } From fed0deac02d8cf132fdfad4b08a546ad173414cc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 23 Sep 2023 23:43:43 +0300 Subject: [PATCH 064/896] Add brief explaination of `BASE_SIZE` --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index f77dab56c8..c5c9556ed7 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -29,6 +29,13 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The size of the catcher at 1x scale. /// + /// + /// This is mainly used to compute catching range, the actual catcher size may differ based on skin implementation and sprite textures. + /// This is also equivalent to the "catcherWidth" property in osu-stable when the game field and beatmap difficulty are set to default values. + /// + /// + /// + /// public const float BASE_SIZE = 106.75f; /// From 693b8d72db961844e2cde901622919127beceee3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 10 Oct 2023 01:23:00 +0300 Subject: [PATCH 065/896] Fix test catcher sprites in "special skin" incorrectly suffixed as being @2x --- ...t-catcher-fail@2x.png => fruit-catcher-fail.png} | Bin ...tcher-idle-0@2x.png => fruit-catcher-idle-0.png} | Bin ...tcher-idle-1@2x.png => fruit-catcher-idle-1.png} | Bin ...tcher-idle-2@2x.png => fruit-catcher-idle-2.png} | Bin ...tcher-idle-3@2x.png => fruit-catcher-idle-3.png} | Bin ...tcher-idle-4@2x.png => fruit-catcher-idle-4.png} | Bin ...tcher-idle-5@2x.png => fruit-catcher-idle-5.png} | Bin ...t-catcher-kiai@2x.png => fruit-catcher-kiai.png} | Bin 8 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-fail@2x.png => fruit-catcher-fail.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-idle-0@2x.png => fruit-catcher-idle-0.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-idle-1@2x.png => fruit-catcher-idle-1.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-idle-2@2x.png => fruit-catcher-idle-2.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-idle-3@2x.png => fruit-catcher-idle-3.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-idle-4@2x.png => fruit-catcher-idle-4.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-idle-5@2x.png => fruit-catcher-idle-5.png} (100%) rename osu.Game.Rulesets.Catch.Tests/Resources/special-skin/{fruit-catcher-kiai@2x.png => fruit-catcher-kiai.png} (100%) diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-fail.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-0.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-1.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-2.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-3.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-4.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-idle-5.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png similarity index 100% rename from osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai@2x.png rename to osu.Game.Rulesets.Catch.Tests/Resources/special-skin/fruit-catcher-kiai.png From b2987caf7cc56248ba6638fc7d9b34708f96c80a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 10 Oct 2023 02:17:43 +0300 Subject: [PATCH 066/896] Adjust the catch playfield's position to match 1:1 with stable --- .../UI/CatchPlayfieldAdjustmentContainer.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index 74cbc665c0..2ba41a38d5 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -17,12 +17,13 @@ namespace osu.Game.Rulesets.Catch.UI public CatchPlayfieldAdjustmentContainer() { - // because we are using centre anchor/origin, we will need to limit visibility in the future - // to ensure tall windows do not get a readability advantage. - // it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values - // which are compatible with TopCentre alignment. - Anchor = Anchor.Centre; - Origin = Anchor.Centre; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + + // playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable. + // we can match that in lazer by using relative coordinates for Y and considering window height to be 1, and playfield height to be 0.8. + RelativePositionAxes = Axes.Y; + Y = (1 - playfield_size_adjust) / 4 * 3; Size = new Vector2(playfield_size_adjust); From 7211c88e83bc1fa1d24c13041be7ea01357db149 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Oct 2023 08:32:35 +0900 Subject: [PATCH 067/896] Make combo assertion actually work as expected --- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 5bc477535d..93128c512f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -200,9 +200,10 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertCombo(1); + assertComboAtJudgement(0, 1); assertTailJudgement(HitResult.Meh); - assertCombo(1); + assertComboAtJudgement(1, 0); + assertComboAtJudgement(2, 1); } /// @@ -521,8 +522,8 @@ namespace osu.Game.Rulesets.Mania.Tests private void assertNoteJudgement(HitResult result) => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result)); - private void assertCombo(int combo) - => AddAssert($"combo is {combo}", () => currentPlayer.ScoreProcessor.Combo.Value, () => Is.EqualTo(combo)); + private void assertComboAtJudgement(int judgementIndex, int combo) + => AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo)); private ScoreAccessibleReplayPlayer currentPlayer = null!; From 6073f3e756536d6765cf18a3cc34196bc15d464a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 10 Oct 2023 02:18:21 +0300 Subject: [PATCH 068/896] Refactor catch playfield size adjustment logic w.r.t. catcher position --- .../UI/CatchPlayfieldAdjustmentContainer.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index 2ba41a38d5..11531011ee 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -43,18 +43,28 @@ namespace osu.Game.Rulesets.Catch.UI /// private partial class ScalingContainer : Container { + public ScalingContainer() + { + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + } + protected override void Update() { base.Update(); - // in stable, fruit fall vertically from -100 to 340. - // to emulate this, we want to make our playfield 440 gameplay pixels high. - // we then offset it -100 vertically in the position set below. - const float stable_v_offset_ratio = 440 / 384f; + // in stable, fruit fall vertically from 100 pixels above the playfield top down to the catcher's Y position (i.e. -100 to 340), + // see: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjects/Fruits/HitCircleFruits.cs#L65 + // we already have the playfield positioned similar to stable (see CatchPlayfieldAdjustmentContainer constructor), + // so we only need to increase this container's height 100 pixels above the playfield, and offset it to have the bottom at 340 rather than 384. + const float stable_fruit_start_position = -100; + const float stable_catcher_y_position = 340; + const float playfield_v_size_adjustment = (stable_catcher_y_position - stable_fruit_start_position) / CatchPlayfield.HEIGHT; + const float playfield_v_catcher_offset = stable_catcher_y_position - CatchPlayfield.HEIGHT; - Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH); - Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X); - Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale); + Scale = new Vector2(Parent!.ChildSize.X / CatchPlayfield.WIDTH); + Position = new Vector2(0f, playfield_v_catcher_offset * Scale.Y); + Size = Vector2.Divide(new Vector2(1, playfield_v_size_adjustment), Scale); } } } From 594bcc2ab533c610909f1533eb2082c6822e1aef Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Oct 2023 09:56:26 +0900 Subject: [PATCH 069/896] Apply suggestion to condense method --- osu.Game/Rulesets/Scoring/HitResult.cs | 102 +++---------------------- 1 file changed, 12 insertions(+), 90 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index cc528e406f..c2788b2dd6 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -331,101 +331,23 @@ namespace osu.Game.Rulesets.Scoring public static void ValidateHitResultPair(HitResult maxResult, HitResult minResult) { - // Check valid maximum judgements. - switch (maxResult) - { - case HitResult.Meh: - case HitResult.Ok: - case HitResult.Good: - case HitResult.Great: - case HitResult.Perfect: - case HitResult.SmallTickHit: - case HitResult.LargeTickHit: - case HitResult.SmallBonus: - case HitResult.LargeBonus: - case HitResult.IgnoreHit: - break; + if (maxResult == HitResult.None || !IsHit(maxResult)) + throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result."); - default: - throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result."); - } + if (minResult == HitResult.None || IsHit(minResult)) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result."); - // Check valid minimum judgements. - switch (minResult) - { - case HitResult.Miss: - case HitResult.SmallTickMiss: - case HitResult.LargeTickMiss: - case HitResult.IgnoreMiss: - case HitResult.ComboBreak: - break; + if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement."); - default: - throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result."); - } + if (maxResult == HitResult.LargeTickHit && minResult is not (HitResult.LargeTickMiss or HitResult.IgnoreMiss or HitResult.ComboBreak)) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); - // Check valid category pairings. - switch (maxResult) - { - case HitResult.SmallBonus: - case HitResult.LargeBonus: - switch (minResult) - { - case HitResult.IgnoreMiss: - break; + if (maxResult == HitResult.SmallTickHit && minResult is not (HitResult.SmallTickMiss or HitResult.IgnoreMiss or HitResult.ComboBreak)) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); - default: - throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement."); - } - - break; - - case HitResult.SmallTickHit: - switch (minResult) - { - case HitResult.SmallTickMiss: - case HitResult.IgnoreMiss: - case HitResult.ComboBreak: - break; - - default: - throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); - } - - break; - - case HitResult.LargeTickHit: - switch (minResult) - { - case HitResult.LargeTickMiss: - case HitResult.IgnoreMiss: - case HitResult.ComboBreak: - break; - - default: - throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); - } - - break; - - case HitResult.Meh: - case HitResult.Ok: - case HitResult.Good: - case HitResult.Great: - case HitResult.Perfect: - switch (minResult) - { - case HitResult.Miss: - case HitResult.IgnoreMiss: - case HitResult.ComboBreak: - break; - - default: - throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); - } - - break; - } + if (maxResult.IsBasic() && minResult is not (HitResult.Miss or HitResult.IgnoreMiss or HitResult.ComboBreak)) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); } } #pragma warning restore CS0618 From 89a3be11cbc7b04ecb95f8ac8a476c335a4f1c0f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Oct 2023 09:56:35 +0900 Subject: [PATCH 070/896] Add validation test --- .../Rulesets/Scoring/HitResultTest.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs diff --git a/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs new file mode 100644 index 0000000000..68d7335055 --- /dev/null +++ b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs @@ -0,0 +1,36 @@ +// 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 NUnit.Framework; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Tests.Rulesets.Scoring +{ + [TestFixture] + public class HitResultTest + { + [TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })] + [TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })] + [TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })] + [TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })] + [TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })] + public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults) + { + HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => !minResults.Contains(t)).ToArray(); + + Assert.Multiple(() => + { + foreach (var max in maxResults) + { + foreach (var min in minResults) + Assert.DoesNotThrow(() => HitResultExtensions.ValidateHitResultPair(max, min), $"{max} + {min} should be supported."); + + foreach (var unsupported in unsupportedResults) + Assert.Throws(() => HitResultExtensions.ValidateHitResultPair(max, unsupported), $"{max} + {unsupported} should not be supported."); + } + }); + } + } +} From 04ce223e66d0d169500e35d636149796461f76e1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Oct 2023 10:03:37 +0900 Subject: [PATCH 071/896] Remove pairings that shouldn't be valid --- osu.Game/Rulesets/Judgements/Judgement.cs | 6 +++--- osu.Game/Rulesets/Scoring/HitResult.cs | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index b9e88dce7a..d0c4c4395d 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Judgements /// /// /// , , , , - /// , , + /// /// /// /// @@ -56,11 +56,11 @@ namespace osu.Game.Rulesets.Judgements /// /// /// - /// , , + /// /// /// /// - /// , , + /// /// /// /// diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index c2788b2dd6..ccd1f49de4 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -337,17 +337,20 @@ namespace osu.Game.Rulesets.Scoring if (minResult == HitResult.None || IsHit(minResult)) throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result."); + if (maxResult == HitResult.IgnoreHit && minResult is not (HitResult.IgnoreMiss or HitResult.ComboBreak)) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); + if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement."); - if (maxResult == HitResult.LargeTickHit && minResult is not (HitResult.LargeTickMiss or HitResult.IgnoreMiss or HitResult.ComboBreak)) - throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); + if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); - if (maxResult == HitResult.SmallTickHit && minResult is not (HitResult.SmallTickMiss or HitResult.IgnoreMiss or HitResult.ComboBreak)) - throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); + if (maxResult == HitResult.SmallTickHit && minResult != HitResult.SmallTickMiss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.SmallTickMiss} is the only valid minimum result for a {maxResult} judgement."); - if (maxResult.IsBasic() && minResult is not (HitResult.Miss or HitResult.IgnoreMiss or HitResult.ComboBreak)) - throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement."); + if (maxResult.IsBasic() && minResult != HitResult.Miss) + throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.Miss} is the only valid minimum result for a {maxResult} judgement."); } } #pragma warning restore CS0618 From 1a60d6ade5c2a7258dc56f7005d54dd005f004cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 13:35:06 +0900 Subject: [PATCH 072/896] Use a `DrawablePool` for mania's beat snap grid I'm not sure what the cause of the issue is, but I'm also not sure why it wasn't using `DrawablePool` (was it not around back then?). Either way, switching to a proper pool fixes things just fine. Resolves https://github.com/ppy/osu/issues/25009. --- .../Edit/ManiaBeatSnapGrid.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 9cc84450cc..4d45e16588 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -8,6 +8,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; @@ -23,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Edit /// /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor. /// - public partial class ManiaBeatSnapGrid : Component + public partial class ManiaBeatSnapGrid : CompositeComponent { private const double visible_range = 750; @@ -53,6 +55,8 @@ namespace osu.Game.Rulesets.Mania.Edit private readonly List grids = new List(); + private readonly DrawablePool linesPool = new DrawablePool(50); + private readonly Cached lineCache = new Cached(); private (double start, double end)? selectionTimeRange; @@ -60,6 +64,8 @@ namespace osu.Game.Rulesets.Mania.Edit [BackgroundDependencyLoader] private void load(HitObjectComposer composer) { + AddInternal(linesPool); + foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages) { foreach (var column in stage.Columns) @@ -85,17 +91,10 @@ namespace osu.Game.Rulesets.Mania.Edit } } - private readonly Stack availableLines = new Stack(); - private void createLines() { foreach (var grid in grids) - { - foreach (var line in grid.Objects.OfType()) - availableLines.Push(line); - grid.Clear(); - } if (selectionTimeRange == null) return; @@ -131,10 +130,13 @@ namespace osu.Game.Rulesets.Mania.Edit foreach (var grid in grids) { - if (!availableLines.TryPop(out var line)) - line = new DrawableGridLine(); + var line = linesPool.Get(); + + line.Apply(new HitObject + { + StartTime = time + }); - line.HitObject.StartTime = time; line.Colour = colour; grid.Add(line); From 8e26b3c4051762e9a4c9f4624b0b5babe14145e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 15:09:33 +0900 Subject: [PATCH 073/896] Fix argon health bar not completing flash animation correctly --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index aa3b6c1f41..4c63479d3d 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -221,6 +221,9 @@ namespace osu.Game.Screens.Play.HUD private void finishMissDisplay() { + if (resetMissBarDelegate == null) + return; + if (Current.Value > 0) { glowBar.TransformTo(nameof(BarPath.BarColour), main_bar_colour, 300, Easing.In); From aeb579eb54b0371682b5fd2ec5146ed56c2e5ee2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 15:10:27 +0900 Subject: [PATCH 074/896] Syncrhonise initial colour of `glowBar` for sanity --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 4c63479d3d..2a42023f83 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -15,7 +15,6 @@ using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -114,7 +113,7 @@ namespace osu.Game.Screens.Play.HUD glowBar = new BarPath { BarColour = Color4.White, - GlowColour = OsuColour.Gray(0.5f), + GlowColour = main_bar_glow_colour, Blending = BlendingParameters.Additive, Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), PathRadius = 40f, From 6ee958743e29bf5ad59f355a20f512fe116e6319 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 15:10:42 +0900 Subject: [PATCH 075/896] Adjust glow animation specifics slightly --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 2a42023f83..a3a35999fa 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -181,12 +181,13 @@ namespace osu.Game.Screens.Play.HUD if (resetMissBarDelegate == null) { - glowBar.TransformTo(nameof(BarPath.BarColour), Colour4.White, 100, Easing.OutQuint) + glowBar.TransformTo(nameof(BarPath.BarColour), Colour4.White, 30, Easing.OutQuint) .Then() - .TransformTo(nameof(BarPath.BarColour), main_bar_colour, 800, Easing.OutQuint); + .TransformTo(nameof(BarPath.BarColour), main_bar_colour, 1000, Easing.OutQuint); - glowBar.TransformTo(nameof(BarPath.GlowColour), Colour4.White) - .TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 800, Easing.OutQuint); + glowBar.TransformTo(nameof(BarPath.GlowColour), Colour4.White, 30, Easing.OutQuint) + .Then() + .TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); } } From 57f588fa8697841524c070567659c8dece6817c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 15:23:50 +0900 Subject: [PATCH 076/896] Ensure health displays don't pile up transforms when off-screen --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 15 +++++++++------ .../Screens/Play/HUD/DefaultHealthDisplay.cs | 16 +++++++--------- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 8 +++++--- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index aa3b6c1f41..b6e2f494d1 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -142,14 +142,17 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(v => { - if (v.NewValue >= GlowBarValue) - finishMissDisplay(); + Scheduler.AddOnce(() => + { + if (v.NewValue >= GlowBarValue) + finishMissDisplay(); - double time = v.NewValue > GlowBarValue ? 500 : 250; + double time = v.NewValue > GlowBarValue ? 500 : 250; - this.TransformTo(nameof(HealthBarValue), v.NewValue, time, Easing.OutQuint); - if (resetMissBarDelegate == null) - this.TransformTo(nameof(GlowBarValue), v.NewValue, time, Easing.OutQuint); + this.TransformTo(nameof(HealthBarValue), v.NewValue, time, Easing.OutQuint); + if (resetMissBarDelegate == null) + this.TransformTo(nameof(GlowBarValue), v.NewValue, time, Easing.OutQuint); + }); }, true); BarLength.BindValueChanged(l => Width = l.NewValue, true); diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index c4d04c5580..0d3bd8d46f 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -112,6 +112,13 @@ namespace osu.Game.Screens.Play.HUD }; } + protected override void Flash(JudgementResult result) + { + fill.FadeEdgeEffectTo(Math.Min(1, fill.EdgeEffect.Colour.Linear.A + (1f - base_glow_opacity) / glow_max_hits), 50, Easing.OutQuint) + .Delay(glow_fade_delay) + .FadeEdgeEffectTo(base_glow_opacity, glow_fade_time, Easing.OutQuint); + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -119,15 +126,6 @@ namespace osu.Game.Screens.Play.HUD GlowColour = colours.BlueDarker; } - protected override void Flash(JudgementResult result) => Scheduler.AddOnce(flash); - - private void flash() - { - fill.FadeEdgeEffectTo(Math.Min(1, fill.EdgeEffect.Colour.Linear.A + (1f - base_glow_opacity) / glow_max_hits), 50, Easing.OutQuint) - .Delay(glow_fade_delay) - .FadeEdgeEffectTo(base_glow_opacity, glow_fade_time, Easing.OutQuint); - } - protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 986efe3036..e297ffc411 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -39,6 +39,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Triggered when a is a successful hit, signaling the health display to perform a flash animation (if designed to do so). + /// Calls to this method are debounced. /// /// The judgement result. protected virtual void Flash(JudgementResult result) @@ -47,6 +48,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Triggered when a resulted in the player losing health. + /// Calls to this method are debounced. /// /// The judgement result. protected virtual void Miss(JudgementResult result) @@ -92,7 +94,7 @@ namespace osu.Game.Screens.Play.HUD { double newValue = Current.Value + 0.05f; this.TransformBindableTo(Current, newValue, increase_delay); - Flash(new JudgementResult(new HitObject(), new Judgement())); + Scheduler.AddOnce(Flash, new JudgementResult(new HitObject(), new Judgement())); if (newValue >= 1) finishInitialAnimation(); @@ -115,9 +117,9 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { if (judgement.IsHit && judgement.Type != HitResult.IgnoreHit) - Flash(judgement); + Scheduler.AddOnce(Flash, judgement); else if (judgement.Judgement.HealthIncreaseFor(judgement) < 0) - Miss(judgement); + Scheduler.AddOnce(Miss, judgement); } protected override void Dispose(bool isDisposing) From 6c346874d3f191c34651943dd3fda84bc3dd2866 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 15:24:03 +0900 Subject: [PATCH 077/896] Add TODO mentioning that `ArgonHealthDisplay` is doing lerp wrong --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index b6e2f494d1..3c29d068e6 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -149,6 +149,7 @@ namespace osu.Game.Screens.Play.HUD double time = v.NewValue > GlowBarValue ? 500 : 250; + // TODO: this should probably use interpolation in update. this.TransformTo(nameof(HealthBarValue), v.NewValue, time, Easing.OutQuint); if (resetMissBarDelegate == null) this.TransformTo(nameof(GlowBarValue), v.NewValue, time, Easing.OutQuint); From 1eb6c93916d015524dad238672492470a2c7c94f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 15:45:35 +0900 Subject: [PATCH 078/896] Also debounce `updatePathVertices` to reduce overhead --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 3c29d068e6..9c7c0684b3 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Play.HUD return; glowBarValue = value; - updatePathVertices(); + Scheduler.AddOnce(updatePathVertices); } } @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Play.HUD return; healthBarValue = value; - updatePathVertices(); + Scheduler.AddOnce(updatePathVertices); } } From 7e68b6452632f48f3a1656633df977f88a069972 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 15:59:48 +0900 Subject: [PATCH 079/896] Avoid running `finishInitialAnimation` when already finished --- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index e297ffc411..212a223e22 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -103,6 +103,9 @@ namespace osu.Game.Screens.Play.HUD private void finishInitialAnimation() { + if (initialIncrease == null) + return; + initialIncrease?.Cancel(); initialIncrease = null; From 4674f6365541e6f9a8c289389fc01cc4903f1212 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 16:17:34 +0900 Subject: [PATCH 080/896] Fix `Scheduler.AddOnce` not working in one location --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 9c7c0684b3..bc095838ab 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -142,18 +142,8 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(v => { - Scheduler.AddOnce(() => - { - if (v.NewValue >= GlowBarValue) - finishMissDisplay(); - - double time = v.NewValue > GlowBarValue ? 500 : 250; - - // TODO: this should probably use interpolation in update. - this.TransformTo(nameof(HealthBarValue), v.NewValue, time, Easing.OutQuint); - if (resetMissBarDelegate == null) - this.TransformTo(nameof(GlowBarValue), v.NewValue, time, Easing.OutQuint); - }); + // For some reason making the delegate inline here doesn't work correctly. + Scheduler.AddOnce(updateCurrent); }, true); BarLength.BindValueChanged(l => Width = l.NewValue, true); @@ -169,6 +159,17 @@ namespace osu.Game.Screens.Play.HUD return base.OnInvalidate(invalidation, source); } + private void updateCurrent() + { + if (Current.Value >= GlowBarValue) finishMissDisplay(); + + double time = Current.Value > GlowBarValue ? 500 : 250; + + // TODO: this should probably use interpolation in update. + this.TransformTo(nameof(HealthBarValue), Current.Value, time, Easing.OutQuint); + if (resetMissBarDelegate == null) this.TransformTo(nameof(GlowBarValue), Current.Value, time, Easing.OutQuint); + } + protected override void Update() { base.Update(); From 08b1a3cbe28f51e05ef75d14a65037e3f98fa858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Oct 2023 09:18:57 +0200 Subject: [PATCH 081/896] Add failing test case for guest user being looked up in API --- .../TestScenePlayerLocalScoreImport.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 6a7fab86d6..8e21c3ea9f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -15,6 +15,9 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -26,6 +29,7 @@ using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Tests.Resources; +using osu.Game.Users; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -150,6 +154,40 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); } + [Test] + public void TestGuestScoreIsStoredAsGuest() + { + AddStep("set up API", () => ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetUserRequest userRequest: + userRequest.TriggerSuccess(new APIUser + { + Username = "Guest", + CountryCode = CountryCode.JP, + Id = 1234 + }); + return true; + + default: + return false; + } + }); + + AddStep("log out", () => API.Logout()); + CreateTest(); + + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); + AddStep("log back in", () => API.Login("username", "password")); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + + AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); + AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null)); + AddAssert("score is not associated with online user", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID))!.UserID == APIUser.SYSTEM_USER_ID); + } + [Test] public void TestReplayExport() { From 24956588e906af6aa39d1f79e95870908f359a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Oct 2023 09:28:01 +0200 Subject: [PATCH 082/896] Fix score importer looking up guest user by username online --- osu.Game/Scoring/ScoreImporter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 886fb1379c..7473d887c3 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -190,6 +190,9 @@ namespace osu.Game.Scoring /// private void populateUserDetails(ScoreInfo model) { + if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID) + return; + string username = model.RealmUser.Username; if (usernameLookupCache.TryGetValue(username, out var existing)) From ea400a9db770fd93bc5ff3cab43ab8f7e82722f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Oct 2023 09:30:51 +0200 Subject: [PATCH 083/896] Fix cross-test interference `TestLastPlayedUpdated` was implicitly relying on running first. `TestGuestScoreIsStoredAsGuest` showing up changed test ordering, causing the former test to fail if it didn't get to run first. --- .../Visual/Gameplay/TestScenePlayerLocalScoreImport.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 8e21c3ea9f..1254aa0639 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -102,6 +102,7 @@ namespace osu.Game.Tests.Visual.Gameplay { DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed); + AddStep("reset last played", () => Realm.Write(r => r.Find(Beatmap.Value.BeatmapInfo.ID)!.LastPlayed = null)); AddAssert("last played is null", () => getLastPlayed() == null); CreateTest(); From 94192644be4f865ad5fb90d1c28de54ced3536d1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Oct 2023 16:51:24 +0900 Subject: [PATCH 084/896] Fix tests --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index ff87b2a833..92e94bd02d 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Rulesets.Scoring for (int i = 0; i < 4; i++) { - var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], beatmap.HitObjects[i].CreateJudgement()) + var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], fourObjectBeatmap.HitObjects[i].CreateJudgement()) { Type = i == 2 ? minResult : hitResult }; @@ -277,8 +277,8 @@ namespace osu.Game.Tests.Rulesets.Scoring { HitObjects = new List { - new TestHitObject(HitResult.Great, HitResult.ComboBreak), - new TestHitObject(HitResult.Great, HitResult.ComboBreak), + new TestHitObject(HitResult.Great), + new TestHitObject(HitResult.IgnoreHit, HitResult.ComboBreak), } }; From 8dc9453d8dd5886e2a92175e9948c9a174a90f32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 16:59:18 +0900 Subject: [PATCH 085/896] Apply some xmldoc and structural changes --- .../Objects/Drawables/DrawableHoldNote.cs | 21 +++++++++++-------- .../Objects/Drawables/DrawableHoldNoteBody.cs | 5 +++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 499f8c99b0..86920927dc 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public double? HoldStartTime { get; private set; } /// - /// Whether the hold note has been released potentially without having caused a break. + /// Used to decide whether to visually clamp the hold note to the judgement line. /// private double? releaseTime; @@ -322,19 +322,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (e.Action != Action.Value) return; - // Make sure a hold was started - if (HoldStartTime == null) - return; - // do not run any of this logic when rewinding, as it inverts order of presses/releases. if ((Clock as IGameplayClock)?.IsRewinding == true) return; - Tail.UpdateResult(); - Body.TriggerResult(Tail.IsHit); + // When our action is released and we are in the middle of a hold, there's a chance that + // the user has released too early (before the tail). + // + // In such a case, we want to record this against the DrawableHoldNoteBody. + if (HoldStartTime != null) + { + Tail.UpdateResult(); + Body.TriggerResult(Tail.IsHit); - endHold(); - releaseTime = Time.Current; + endHold(); + releaseTime = Time.Current; + } } private void endHold() diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs index ded88af145..1b2efbafdf 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteBody.cs @@ -23,8 +23,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables internal void TriggerResult(bool hit) { - if (!AllJudged) - ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); + if (AllJudged) return; + + ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); } } } From 18d20cad8b896b8910ab4000b6f667e6e6344068 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Oct 2023 17:04:21 +0900 Subject: [PATCH 086/896] Adjust xmldoc for readability Co-authored-by: Dean Herbert --- osu.Game/Rulesets/Judgements/Judgement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index d0c4c4395d..f60b3a6c02 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Judgements /// Defaults to a sane value for the given . May be overridden to provide a supported custom value: /// /// - /// Valid s + /// s /// Valid s /// /// From 4319b22db825fd39f99862642d42a6bc213821d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 17:55:35 +0900 Subject: [PATCH 087/896] Add failing test coverage of player not pausing track correctly --- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 45c5c67fff..cbeff770c9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -11,6 +11,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Multiplayer { @@ -28,6 +29,11 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); }); + AddStep("Start track playing", () => + { + Beatmap.Value.Track.Start(); + }); + AddStep("initialise gameplay", () => { Stack.Push(player = new MultiplayerPlayer(MultiplayerClient.ServerAPIRoom, new PlaylistItem(Beatmap.Value.BeatmapInfo) @@ -37,7 +43,14 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded); + + AddAssert("gameplay clock is paused", () => player.ChildrenOfType().Single().IsPaused.Value); + AddAssert("gameplay clock is not running", () => !player.ChildrenOfType().Single().IsRunning); + AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).GameplayStarted()); + + AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType().Single().IsPaused.Value); + AddAssert("gameplay clock is running", () => player.ChildrenOfType().Single().IsRunning); } [Test] From eee7dc02da9611c00ac3d6bc16343507b11728a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 17:58:11 +0900 Subject: [PATCH 088/896] Reduce severity of clock running check to avoid production crashes --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ac9ce11cb5..91131b0aba 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1078,7 +1078,7 @@ namespace osu.Game.Screens.Play protected virtual void StartGameplay() { if (GameplayClockContainer.IsRunning) - throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); + Logger.Error(new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"), "Clock failure"); GameplayClockContainer.Reset(startClock: true); From 100c0cf5331ad7806424acfb7d1e3371c39553c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 17:58:37 +0900 Subject: [PATCH 089/896] Fix `MultiplayerPlayer` never calling `Reset` on `GameplayClockContainer` This call is responsible for ensuring the clock is in a stopped state. --- osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 7b448e4b5c..e6d9dd4cd0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -148,6 +148,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } + + // This will pause the clock, pending the gameplay started callback from the server. + GameplayClockContainer.Reset(); } private void failAndBail(string message = null) From f0bd97539379ad11f54dbdf354f0adca7565044a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 18:16:12 +0900 Subject: [PATCH 090/896] Change `GameplayClockContainer.Reset` to directly call `GameplayClock.Stop` The reasoning is explained in the inline comment, but basically this was getting blocked by `isPaused` being in an initial `true` state (as it is on construction), while the source clock was still `IsRunning`. There's no real guarantee of sync between the source and the `isPaused` bindable right now. Maybe there should be in the future, but to restore sanity, let's ensure that a call to `Reset` can at least stop the track as we expect. --- osu.Game/Screens/Play/GameplayClockContainer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index e6a38a9946..3c920babee 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -138,12 +138,15 @@ namespace osu.Game.Screens.Play /// Resets this and the source to an initial state ready for gameplay. /// /// The time to seek to on resetting. If null, the existing will be used. - /// Whether to start the clock immediately, if not already started. + /// Whether to start the clock immediately. If false, the clock will remain stopped after this call. public void Reset(double? time = null, bool startClock = false) { bool wasPaused = isPaused.Value; - Stop(); + // The intention of the Reset method is to get things into a known sane state. + // As such, we intentionally stop the underlying clock directly here, bypassing Stop/StopGameplayClock. + // This is to avoid any kind of isPaused state checks and frequency ramping (as provided by MasterGameplayClockContainer). + GameplayClock.Stop(); if (time != null) StartTime = time.Value; From 97e521acad28cdbcc7db526623038ebb1e6ea4a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 18:47:20 +0900 Subject: [PATCH 091/896] Reduce black fill of song bar Closes https://github.com/ppy/osu/issues/24993. --- osu.Game.Tournament/Components/SongBar.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index cde826628e..cc1d00f62f 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -84,6 +84,7 @@ namespace osu.Game.Tournament.Components { Colour = colours.Gray3, RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, }, flow = new FillFlowContainer { From 7220ca34f7dfc6f8e1eda76975712f02fe621322 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 19:05:01 +0900 Subject: [PATCH 092/896] Add failing test coverage of incorrect default preview time --- .../Formats/LegacyBeatmapDecoderTest.cs | 28 ++++++++++++++++--- .../Resources/beatmap-version-4.osu | 4 +++ ...tmap-version.osu => beatmap-version-6.osu} | 0 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Resources/beatmap-version-4.osu rename osu.Game.Tests/Resources/{beatmap-version.osu => beatmap-version-6.osu} (100%) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 3d2de2914a..fec7f86eb2 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapVersion() { - using (var resStream = TestResources.OpenResource("beatmap-version.osu")) + using (var resStream = TestResources.OpenResource("beatmap-version-6.osu")) using (var stream = new LineBufferedReader(resStream)) { var decoder = Decoder.GetDecoder(stream); @@ -45,6 +45,25 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [TestCase(false)] + [TestCase(true)] + public void TestPreviewPointWithOffsets(bool applyOffsets) + { + using (var resStream = TestResources.OpenResource("beatmap-version-4.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoder = Decoder.GetDecoder(stream); + ((LegacyBeatmapDecoder)decoder).ApplyOffsets = applyOffsets; + var working = new TestWorkingBeatmap(decoder.Decode(stream)); + + Assert.AreEqual(4, working.BeatmapInfo.BeatmapVersion); + Assert.AreEqual(4, working.Beatmap.BeatmapInfo.BeatmapVersion); + Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty()).BeatmapInfo.BeatmapVersion); + + Assert.AreEqual(-1, working.BeatmapInfo.Metadata.PreviewTime); + } + } + [Test] public void TestDecodeBeatmapGeneral() { @@ -915,10 +934,11 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - [Test] - public void TestLegacyDefaultsPreserved() + [TestCase(false)] + [TestCase(true)] + public void TestLegacyDefaultsPreserved(bool applyOffsets) { - var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = applyOffsets }; using (var memoryStream = new MemoryStream()) using (var stream = new LineBufferedReader(memoryStream)) diff --git a/osu.Game.Tests/Resources/beatmap-version-4.osu b/osu.Game.Tests/Resources/beatmap-version-4.osu new file mode 100644 index 0000000000..bba4ed46f1 --- /dev/null +++ b/osu.Game.Tests/Resources/beatmap-version-4.osu @@ -0,0 +1,4 @@ +osu file format v4 + +[General] +PreviewTime: -1 diff --git a/osu.Game.Tests/Resources/beatmap-version.osu b/osu.Game.Tests/Resources/beatmap-version-6.osu similarity index 100% rename from osu.Game.Tests/Resources/beatmap-version.osu rename to osu.Game.Tests/Resources/beatmap-version-6.osu From 27818461470c4892a3c5f9e916beffd5929521b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 19:02:04 +0900 Subject: [PATCH 093/896] Fix beatmaps with no preview point set getting incorrect default Note that this will require a re-import to fix existing beatmaps becuse.. it's stored to realm. Probably not worth worrying about for now. --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 37aa7950d3..8559ecfe69 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -196,7 +196,8 @@ namespace osu.Game.Beatmaps.Formats break; case @"PreviewTime": - metadata.PreviewTime = getOffsetTime(Parsing.ParseInt(pair.Value)); + int time = Parsing.ParseInt(pair.Value); + metadata.PreviewTime = time == -1 ? time : getOffsetTime(time); break; case @"SampleSet": From a1a46e58fc09d8066d33ebbe98337fe1a33c3b16 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 10 Oct 2023 21:42:37 +0900 Subject: [PATCH 094/896] Move validation to DHO.ApplyResult() --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 ++ osu.Game/Rulesets/Scoring/JudgementProcessor.cs | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index e31656e0ff..3bb0e3dfb8 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -672,6 +672,8 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); + HitResultExtensions.ValidateHitResultPair(Result.Judgement.MaxResult, Result.Judgement.MinResult); + if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult)) { throw new InvalidOperationException( diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index bf6c2bfe28..e9f3bcb949 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -151,8 +151,6 @@ namespace osu.Game.Rulesets.Scoring { var judgement = obj.CreateJudgement(); - HitResultExtensions.ValidateHitResultPair(judgement.MaxResult, judgement.MinResult); - var result = CreateResult(obj, judgement); if (result == null) throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); From 682aa06acf1928bbf08171d6edbf510a1c8d6fc8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 22:10:44 +0900 Subject: [PATCH 095/896] Remove `JudgementResult` from `Miss`/`Flash` as it is not used --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 9 ++++----- osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs | 3 +-- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 13 +++++-------- osu.Game/Skinning/LegacyHealthDisplay.cs | 7 +++---- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index bc095838ab..d101a8fd64 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -16,7 +16,6 @@ using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; @@ -178,9 +177,9 @@ namespace osu.Game.Screens.Play.HUD glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, GlowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); } - protected override void Flash(JudgementResult result) + protected override void Flash() { - base.Flash(result); + base.Flash(); mainBar.TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour.Opacity(0.8f)) .TransformTo(nameof(BarPath.GlowColour), main_bar_glow_colour, 300, Easing.OutQuint); @@ -196,9 +195,9 @@ namespace osu.Game.Screens.Play.HUD } } - protected override void Miss(JudgementResult result) + protected override void Miss() { - base.Miss(result); + base.Miss(); if (resetMissBarDelegate != null) { diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index 0d3bd8d46f..f531b1c609 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Game.Graphics; -using osu.Game.Rulesets.Judgements; using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; @@ -112,7 +111,7 @@ namespace osu.Game.Screens.Play.HUD }; } - protected override void Flash(JudgementResult result) + protected override void Flash() { fill.FadeEdgeEffectTo(Math.Min(1, fill.EdgeEffect.Colour.Linear.A + (1f - base_glow_opacity) / glow_max_hits), 50, Easing.OutQuint) .Delay(glow_fade_delay) diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index 212a223e22..2f96233939 100644 --- a/osu.Game/Screens/Play/HUD/HealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HealthDisplay.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -41,8 +40,7 @@ namespace osu.Game.Screens.Play.HUD /// Triggered when a is a successful hit, signaling the health display to perform a flash animation (if designed to do so). /// Calls to this method are debounced. ///
- /// The judgement result. - protected virtual void Flash(JudgementResult result) + protected virtual void Flash() { } @@ -50,8 +48,7 @@ namespace osu.Game.Screens.Play.HUD /// Triggered when a resulted in the player losing health. /// Calls to this method are debounced. /// - /// The judgement result. - protected virtual void Miss(JudgementResult result) + protected virtual void Miss() { } @@ -94,7 +91,7 @@ namespace osu.Game.Screens.Play.HUD { double newValue = Current.Value + 0.05f; this.TransformBindableTo(Current, newValue, increase_delay); - Scheduler.AddOnce(Flash, new JudgementResult(new HitObject(), new Judgement())); + Scheduler.AddOnce(Flash); if (newValue >= 1) finishInitialAnimation(); @@ -120,9 +117,9 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { if (judgement.IsHit && judgement.Type != HitResult.IgnoreHit) - Scheduler.AddOnce(Flash, judgement); + Scheduler.AddOnce(Flash); else if (judgement.Judgement.HealthIncreaseFor(judgement) < 0) - Scheduler.AddOnce(Miss, judgement); + Scheduler.AddOnce(Miss); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 08add79fc1..845fc77394 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; -using osu.Game.Rulesets.Judgements; using osu.Game.Screens.Play.HUD; using osu.Game.Utils; using osuTK; @@ -80,7 +79,7 @@ namespace osu.Game.Skinning marker.Position = fill.Position + new Vector2(fill.DrawWidth, isNewStyle ? fill.DrawHeight / 2 : 0); } - protected override void Flash(JudgementResult result) => marker.Flash(result); + protected override void Flash() => marker.Flash(); private static Texture getTexture(ISkin skin, string name) => skin?.GetTexture($"scorebar-{name}"); @@ -238,7 +237,7 @@ namespace osu.Game.Skinning }); } - public override void Flash(JudgementResult result) + public override void Flash() { bulgeMain(); @@ -257,7 +256,7 @@ namespace osu.Game.Skinning { public Bindable Current { get; } = new Bindable(); - public virtual void Flash(JudgementResult result) + public virtual void Flash() { } } From e2e28ef0ca07908b8430ed8c2ddd31fdf308e80d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 22:12:20 +0900 Subject: [PATCH 096/896] Remove comment regarding inlined delegate --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index d101a8fd64..9643eb8b48 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -139,11 +139,7 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - Current.BindValueChanged(v => - { - // For some reason making the delegate inline here doesn't work correctly. - Scheduler.AddOnce(updateCurrent); - }, true); + Current.BindValueChanged(_ => Scheduler.AddOnce(updateCurrent), true); BarLength.BindValueChanged(l => Width = l.NewValue, true); BarHeight.BindValueChanged(_ => updatePath()); From b9dadc52b7c571f4e77e4295b83df69384803ae9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Oct 2023 22:54:19 +0900 Subject: [PATCH 097/896] Adjust comment to match current behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Screens/Play/GameplayClockContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 3c920babee..5a713fdae7 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -138,7 +138,8 @@ namespace osu.Game.Screens.Play /// Resets this and the source to an initial state ready for gameplay. /// /// The time to seek to on resetting. If null, the existing will be used. - /// Whether to start the clock immediately. If false, the clock will remain stopped after this call. + /// Whether to start the clock immediately. If false and the clock was already paused, the clock will remain paused after this call. + /// public void Reset(double? time = null, bool startClock = false) { bool wasPaused = isPaused.Value; From 1acc02405e9cda5f4d419451ab754b05be86f498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 08:18:48 +0200 Subject: [PATCH 098/896] Reorder `KeyBindingRow` members --- .../Settings/Sections/Input/KeyBindingRow.cs | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 1e2283b58b..8fcf519a2e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -39,14 +39,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// public Action BindingUpdated { get; set; } - private readonly object action; - private readonly IEnumerable bindings; + public bool AllowMainMouseButtons; - private const float transition_time = 150; + public IEnumerable Defaults; - private const float height = 20; - - private const float padding = 5; + #region IFilterable private bool matchingFilter; @@ -60,23 +57,39 @@ namespace osu.Game.Overlays.Settings.Sections.Input } } - private Container content; - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => - content.ReceivePositionalInputAt(screenSpacePos); - public bool FilteringActive { get; set; } + public IEnumerable FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text); + + #endregion + + private readonly object action; + private readonly IEnumerable bindings; + + private Bindable isDefault { get; } = new BindableBool(true); + [Resolved] private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } + [Resolved] + private RealmAccess realm { get; set; } + + private Container content; + private OsuSpriteText text; private FillFlowContainer cancelAndClearButtons; private FillFlowContainer buttons; - private Bindable isDefault { get; } = new BindableBool(true); + private KeyButton bindTarget; - public IEnumerable FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text); + private const float transition_time = 150; + private const float height = 20; + private const float padding = 5; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + content.ReceivePositionalInputAt(screenSpacePos); + + public override bool AcceptsFocus => bindTarget == null; public KeyBindingRow(object action, List bindings) { @@ -87,9 +100,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input AutoSizeAxes = Axes.Y; } - [Resolved] - private RealmAccess realm { get; set; } - [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { @@ -205,14 +215,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input base.OnHoverLost(e); } - public override bool AcceptsFocus => bindTarget == null; - - private KeyButton bindTarget; - - public bool AllowMainMouseButtons; - - public IEnumerable Defaults; - private bool isModifier(Key k) => k < Key.F1; protected override bool OnClick(ClickEvent e) => true; From 7ac874d17ac68e966953e8cd03d6d7fb843aea14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 08:20:25 +0200 Subject: [PATCH 099/896] Use init-only properties where applicable --- osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs | 6 +++--- .../Settings/Sections/Input/KeyBindingsSubsection.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 8fcf519a2e..ac09843c96 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -37,11 +37,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// /// Invoked when the binding of this row is updated with a change being written. /// - public Action BindingUpdated { get; set; } + public Action BindingUpdated { get; init; } - public bool AllowMainMouseButtons; + public bool AllowMainMouseButtons { get; init; } - public IEnumerable Defaults; + public IEnumerable Defaults { get; init; } #region IFilterable diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index d6d4abfa92..b138f54644 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// protected virtual bool AutoAdvanceTarget => false; - protected IEnumerable Defaults; + protected IEnumerable Defaults { get; init; } public RulesetInfo Ruleset { get; protected set; } From 0b2b065383b337051e02892e306a21c1496ea7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 08:21:19 +0200 Subject: [PATCH 100/896] Add xmldoc --- .../Overlays/Settings/Sections/Input/KeyBindingRow.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index ac09843c96..54a51069e8 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -39,8 +39,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// public Action BindingUpdated { get; init; } + /// + /// Whether left and right mouse button clicks should be included in the edited bindings. + /// public bool AllowMainMouseButtons { get; init; } + /// + /// The default key bindings for this row. + /// public IEnumerable Defaults { get; init; } #region IFilterable @@ -91,6 +97,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input public override bool AcceptsFocus => bindTarget == null; + /// + /// Creates a new . + /// + /// The action that this row contains bindings for. + /// The keybindings to display in this row. public KeyBindingRow(object action, List bindings) { this.action = action; From 797109c05cf7c16e730fa4b10ab9f2f61f12971c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 08:30:04 +0200 Subject: [PATCH 101/896] Enable NRT in `KeyBindingRow` --- .../Settings/Sections/Input/KeyBindingRow.cs | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 54a51069e8..c0de7a1d16 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -1,15 +1,15 @@ // 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.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// /// Invoked when the binding of this row is updated with a change being written. /// - public Action BindingUpdated { get; init; } + public Action? BindingUpdated { get; init; } /// /// Whether left and right mouse button clicks should be included in the edited bindings. @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// /// The default key bindings for this row. /// - public IEnumerable Defaults { get; init; } + public IEnumerable Defaults { get; init; } = Array.Empty(); #region IFilterable @@ -75,18 +75,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input private Bindable isDefault { get; } = new BindableBool(true); [Resolved] - private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; [Resolved] - private RealmAccess realm { get; set; } + private RealmAccess realm { get; set; } = null!; - private Container content; + private Container content = null!; - private OsuSpriteText text; - private FillFlowContainer cancelAndClearButtons; - private FillFlowContainer buttons; + private OsuSpriteText text = null!; + private FillFlowContainer cancelAndClearButtons = null!; + private FillFlowContainer buttons = null!; - private KeyButton bindTarget; + private KeyButton? bindTarget; private const float transition_time = 150; private const float height = 20; @@ -232,7 +232,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input protected override bool OnMouseDown(MouseDownEvent e) { - if (!HasFocus || !bindTarget.IsHovered) + if (!HasFocus) + return base.OnMouseDown(e); + + Debug.Assert(bindTarget != null); + + if (!bindTarget.IsHovered) return base.OnMouseDown(e); if (!AllowMainMouseButtons) @@ -258,6 +263,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input return; } + Debug.Assert(bindTarget != null); + if (bindTarget.IsHovered) finalise(false); // prevent updating bind target before clear button's action @@ -269,6 +276,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { if (HasFocus) { + Debug.Assert(bindTarget != null); + if (bindTarget.IsHovered) { bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState, e.ScrollDelta), KeyCombination.FromScrollDelta(e.ScrollDelta).First()); @@ -285,6 +294,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus || e.Repeat) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromKey(e.Key)); if (!isModifier(e.Key)) finalise(); @@ -307,6 +318,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromJoystickButton(e.Button)); finalise(); @@ -329,6 +342,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromMidiKey(e.Key)); finalise(); @@ -351,6 +366,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletAuxiliaryButton(e.Button)); finalise(); @@ -373,6 +390,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (!HasFocus) return false; + Debug.Assert(bindTarget != null); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletPenButton(e.Button)); finalise(); @@ -486,10 +505,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input public readonly OsuSpriteText Text; [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private OverlayColourProvider colourProvider { get; set; } = null!; [Resolved] - private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } + private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; private bool isBinding; @@ -612,7 +631,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { base.Dispose(isDisposing); - if (keyCombinationProvider != null) + if (keyCombinationProvider.IsNotNull()) keyCombinationProvider.KeymapChanged -= updateKeyCombinationText; } } From 63843c79c3b3333e4cd8df844ae99257ed0579f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Oct 2023 21:12:04 +0900 Subject: [PATCH 102/896] Amend diffcalc to use something closer to the original calculation for now --- .../Preprocessing/OsuDifficultyHitObject.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 5c2c85d69e..f37452a79c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -215,13 +215,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing if (slider.LazyEndPosition != null) return; - double trackingEndTime = Math.Max( - // SliderTailCircle always occurs at the final end time of the slider, but the player only needs to hold until within a lenience before it. - slider.NestedHitObjects.OfType().Single().StartTime + SliderEventGenerator.TAIL_LENIENCY, - // There's an edge case where one or more ticks/repeats fall within that leniency range. - // In such a case, the player needs to track until the final tick or repeat. - slider.NestedHitObjects.LastOrDefault(n => n is not SliderTailCircle)?.StartTime ?? double.MinValue - ); + // This commented version is actually correct by the new lazer implementation, but intentionally held back from + // difficulty calculator to preserve known behaviour. + // double trackingEndTime = Math.Max( + // // SliderTailCircle always occurs at the final end time of the slider, but the player only needs to hold until within a lenience before it. + // slider.Duration + SliderEventGenerator.TAIL_LENIENCY, + // // There's an edge case where one or more ticks/repeats fall within that leniency range. + // // In such a case, the player needs to track until the final tick or repeat. + // slider.NestedHitObjects.LastOrDefault(n => n is not SliderTailCircle)?.StartTime ?? double.MinValue + // ); + + double trackingEndTime = Math.Max(Math.Max( + slider.StartTime + slider.Duration + SliderEventGenerator.TAIL_LENIENCY, + slider.StartTime + slider.Duration / 2 + ), slider.NestedHitObjects.OfType().Last().StartTime); slider.LazyTravelTime = trackingEndTime - slider.StartTime; From 3f29f27cd48261f7da0f96659fbc494177b0192e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 19:40:55 +0200 Subject: [PATCH 103/896] Assign empty hit windows to `HoldNoteBody` It's not a timed object, so following precedent, it should have empty hitwindows. This is not actually just aesthetics; several components check whether a hitobject has empty hitwindows to determine whether to include it on various HUD displays and results screen components where timed objects are explicitly involved. --- osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs index 92baa25fa9..47163d0d81 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs @@ -3,6 +3,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects { @@ -15,5 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects public class HoldNoteBody : ManiaHitObject { public override Judgement CreateJudgement() => new HoldNoteBodyJudgement(); + protected override HitWindows CreateHitWindows() => HitWindows.Empty; } } From 041318e3e8b1e6bc139bdc1d72407c0a90598328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 19:44:46 +0200 Subject: [PATCH 104/896] Assign red colour to `ComboBreak` hit result For a judgement result to show up on the colour hit error meter, it has to be two things: be scorable, and not be bonus. `ComboBreak` happens to meet both of those criteria - it impacts score indirectly via decreasing the combo portion, and isn't bonus. Therefore it would show up on the colour hit error meter, but because `OsuColour.ForHitResult()` wasn't handling it explicitly, it was painting the result in light blue, which isn't ideal for a miss type judgement. Therefore, use red instead. There is possibly an argument to be made that this judgement should not show up on the colour hit error meter at all, but I think it's actually okay for it to be this way. In any case it doesn't appear to be anything so major as to warrant blocking the hold note tick removal at this time. --- osu.Game/Graphics/OsuColour.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 1b21f79c0a..d0e07a9e66 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -78,6 +78,7 @@ namespace osu.Game.Graphics case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: case HitResult.Miss: + case HitResult.ComboBreak: return Red; case HitResult.Meh: From 712564ea4f5b527e6c0ea1a6789a1446e4f0f443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Oct 2023 08:49:52 +0200 Subject: [PATCH 105/896] Enable NRT in `KeyBindingsSubsection` --- .../TestSceneChangeAndUseGameplayBindings.cs | 2 +- .../Settings/Sections/Input/KeyBindingsSubsection.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs index 5467a64b85..3a3af43cb1 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneChangeAndUseGameplayBindings.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Navigation private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel .ChildrenOfType() - .FirstOrDefault(s => s.Ruleset.ShortName == "osu"); + .FirstOrDefault(s => s.Ruleset!.ShortName == "osu"); private OsuButton configureBindingsButton => Game.Settings .ChildrenOfType().SingleOrDefault()? diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index b138f54644..8285204bb3 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -1,13 +1,13 @@ // 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.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Input.Bindings; @@ -25,9 +25,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input /// protected virtual bool AutoAdvanceTarget => false; - protected IEnumerable Defaults { get; init; } + protected IEnumerable Defaults { get; init; } = Array.Empty(); - public RulesetInfo Ruleset { get; protected set; } + public RulesetInfo? Ruleset { get; protected set; } private readonly int? variant; @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input [BackgroundDependencyLoader] private void load(RealmAccess realm) { - string rulesetName = Ruleset?.ShortName; + string? rulesetName = Ruleset?.ShortName; var bindings = realm.Run(r => r.All() .Where(b => b.RulesetName == rulesetName && b.Variant == variant) From 5ffc25c8e847d97b678de8946676338ed8ba379b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 03:19:43 +0900 Subject: [PATCH 106/896] Fix potential failure when slider has no ticks --- .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index f37452a79c..959449fdbb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing double trackingEndTime = Math.Max(Math.Max( slider.StartTime + slider.Duration + SliderEventGenerator.TAIL_LENIENCY, slider.StartTime + slider.Duration / 2 - ), slider.NestedHitObjects.OfType().Last().StartTime); + ), slider.NestedHitObjects.OfType().LastOrDefault()?.StartTime ?? double.MinValue); slider.LazyTravelTime = trackingEndTime - slider.StartTime; From 764f641bc9037c83c769f41230433cf79f1217a2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 11 Oct 2023 12:47:12 -0700 Subject: [PATCH 107/896] Add extended info support to tiny mod switches --- .../UserInterface/TestSceneModSwitchTiny.cs | 60 +++++++++- osu.Game/Rulesets/UI/ModSwitchTiny.cs | 112 ++++++++++++++---- 2 files changed, 146 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs index 2d953fe9ad..b7cf3fef9b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSwitchTiny.cs @@ -2,16 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.UI; @@ -34,6 +37,49 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestMania() => createSwitchTestFor(new ManiaRuleset()); + [Test] + public void TestShowRateAdjusts() + { + AddStep("create mod icons", () => + { + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Full, + ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods() + .OfType() + .SelectMany(m => + { + List icons = new List { new TestModSwitchTiny(m) }; + + for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10) + { + m = (ModRateAdjust)m.DeepClone(); + m.SpeedChange.Value = i; + icons.Add(new TestModSwitchTiny(m, true)); + } + + return icons; + }), + }; + }); + + AddStep("adjust rates", () => + { + foreach (var icon in this.ChildrenOfType()) + { + if (icon.Mod is ModRateAdjust rateAdjust) + { + rateAdjust.SpeedChange.Value = RNG.NextDouble() > 0.9 + ? rateAdjust.SpeedChange.Default + : RNG.NextDouble(rateAdjust.SpeedChange.MinValue, rateAdjust.SpeedChange.MaxValue); + } + } + }); + + AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active)); + } + private void createSwitchTestFor(Ruleset ruleset) { AddStep("no colour scheme", () => Child = createContent(ruleset, null)); @@ -43,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep($"{scheme} colour scheme", () => Child = createContent(ruleset, scheme)); } - AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active)); + AddToggleStep("toggle active", active => this.ChildrenOfType().ForEach(s => s.Active.Value = active)); } private static Drawable createContent(Ruleset ruleset, OverlayColourScheme? colourScheme) @@ -62,7 +108,7 @@ namespace osu.Game.Tests.Visual.UserInterface AutoSizeAxes = Axes.Y, Direction = FillDirection.Full, Spacing = new Vector2(5), - ChildrenEnumerable = group.Select(mod => new ModSwitchTiny(mod)) + ChildrenEnumerable = group.Select(mod => new TestModSwitchTiny(mod)) }) }; @@ -81,5 +127,15 @@ namespace osu.Game.Tests.Visual.UserInterface return switchFlow; } + + private partial class TestModSwitchTiny : ModSwitchTiny + { + public new IMod Mod => base.Mod; + + public TestModSwitchTiny(IMod mod, bool showExtendedInformation = false) + : base(mod, showExtendedInformation) + { + } + } } } diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index a5cf75bd07..a3e325ace8 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -3,15 +3,16 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.UI @@ -21,8 +22,10 @@ namespace osu.Game.Rulesets.UI public BindableBool Active { get; } = new BindableBool(); public const float DEFAULT_HEIGHT = 30; + private const float width = 73; - private readonly IMod mod; + protected readonly IMod Mod; + private readonly bool showExtendedInformation; private readonly Box background; private readonly OsuSpriteText acronymText; @@ -33,33 +36,69 @@ namespace osu.Game.Rulesets.UI private Color4 activeBackgroundColour; private Color4 inactiveBackgroundColour; - public ModSwitchTiny(IMod mod) - { - this.mod = mod; - Size = new Vector2(73, DEFAULT_HEIGHT); + private readonly CircularContainer extendedContent; + private readonly Box extendedBackground; + private readonly OsuSpriteText extendedText; + private ModSettingChangeTracker? modSettingsChangeTracker; - InternalChild = new CircularContainer + public ModSwitchTiny(IMod mod, bool showExtendedInformation = false) + { + Mod = mod; + this.showExtendedInformation = showExtendedInformation; + AutoSizeAxes = Axes.X; + Height = DEFAULT_HEIGHT; + + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] + extendedContent = new CircularContainer { - background = new Box + Name = "extended content", + Width = 100 + DEFAULT_HEIGHT / 2, + RelativeSizeAxes = Axes.Y, + Masking = true, + X = width, + Margin = new MarginPadding { Left = -DEFAULT_HEIGHT }, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - acronymText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black), - Text = mod.Acronym, - Margin = new MarginPadding + extendedBackground = new Box { - Top = 4 - } + RelativeSizeAxes = Axes.Both, + }, + extendedText = new OsuSpriteText + { + Margin = new MarginPadding { Left = 3 * DEFAULT_HEIGHT / 4 }, + Font = OsuFont.Default.With(size: 30f, weight: FontWeight.Bold), + UseFullGlyphHeight = false, + Text = mod.ExtendedIconInformation, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, } + }, + new CircularContainer + { + Width = width, + RelativeSizeAxes = Axes.Y, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + acronymText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, + Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black), + Text = mod.Acronym, + Margin = new MarginPadding + { + Top = 4 + } + }, + }, } }; } @@ -68,7 +107,7 @@ namespace osu.Game.Rulesets.UI private void load(OsuColour colours, OverlayColourProvider? colourProvider) { inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3; - activeBackgroundColour = colours.ForModType(mod.Type); + activeBackgroundColour = colours.ForModType(Mod.Type); inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5; activeForegroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1); @@ -80,12 +119,37 @@ namespace osu.Game.Rulesets.UI Active.BindValueChanged(_ => updateState(), true); FinishTransforms(true); + + if (Mod is Mod actualMod) + { + modSettingsChangeTracker = new ModSettingChangeTracker(new[] { actualMod }); + modSettingsChangeTracker.SettingChanged = _ => updateExtendedInformation(); + } + + updateExtendedInformation(); + } + + private void updateExtendedInformation() + { + bool showExtended = showExtendedInformation && !string.IsNullOrEmpty(Mod.ExtendedIconInformation); + + extendedContent.Alpha = showExtended ? 1 : 0; + extendedText.Text = Mod.ExtendedIconInformation; } private void updateState() { acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint); background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint); + + extendedText.Colour = Active.Value ? activeBackgroundColour.Lighten(0.2f) : inactiveBackgroundColour; + extendedBackground.Colour = Active.Value ? activeBackgroundColour.Darken(2.4f) : inactiveBackgroundColour.Darken(2.8f); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + modSettingsChangeTracker?.Dispose(); } } } From 29eba3c58ac14df176150b8680fb197f1aae7518 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Oct 2023 14:05:06 +0900 Subject: [PATCH 108/896] 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 b3feccbbc0..0575817460 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + + From 910d74fdda85433e2cf6b1f62daa3edc4a75f816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 11:59:38 +0200 Subject: [PATCH 366/896] Add failing test coverage for double-click on disabled sliders --- .../UserInterface/TestSceneRoundedSliderBar.cs | 17 +++++++++++++++++ .../UserInterface/TestSceneShearedSliderBar.cs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs index 419e88137c..ae52c26fd2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs @@ -55,5 +55,22 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("slider is default", () => slider.Current.IsDefault); } + + [Test] + public void TestNubDoubleClickOnDisabledSliderDoesNothing() + { + AddStep("set slider to 1", () => slider.Current.Value = 1); + AddStep("disable slider", () => slider.Current.Disabled = true); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1)); + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index d459a2c701..334fc4563a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -55,5 +55,22 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("slider is default", () => slider.Current.IsDefault); } + + [Test] + public void TestNubDoubleClickOnDisabledSliderDoesNothing() + { + AddStep("set slider to 1", () => slider.Current.Value = 1); + AddStep("disable slider", () => slider.Current.Disabled = true); + + AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType().Single())); + + AddStep("double click nub", () => + { + InputManager.Click(MouseButton.Left); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1)); + } } } From 96437c4518a394c2354853b95c5f07853c6340d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 12:03:35 +0200 Subject: [PATCH 367/896] Fix tests specifying float precision for double bindable Would cause assignments to `.Value` to become imprecise even if the underlying bindable could represent the value 100% accurately. --- .../Visual/UserInterface/TestSceneRoundedSliderBar.cs | 2 +- .../Visual/UserInterface/TestSceneShearedSliderBar.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs index ae52c26fd2..311034d595 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly BindableDouble current = new BindableDouble(5) { - Precision = 0.1f, + Precision = 0.1, MinValue = 0, MaxValue = 15 }; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index 334fc4563a..a5072b4e60 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly BindableDouble current = new BindableDouble(5) { - Precision = 0.1f, + Precision = 0.1, MinValue = 0, MaxValue = 15 }; From 89fec95b016364d2a97a30a1d6cbbbf6e46e19a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 12:11:25 +0200 Subject: [PATCH 368/896] Fix cross-test data dependency by recreating slider every time --- .../TestSceneRoundedSliderBar.cs | 22 +++++++++---------- .../TestSceneShearedSliderBar.cs | 22 +++++++++---------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs index 311034d595..66d54c8562 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedSliderBar.cs @@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly BindableDouble current = new BindableDouble(5) - { - Precision = 0.1, - MinValue = 0, - MaxValue = 15 - }; - private RoundedSliderBar slider = null!; - [BackgroundDependencyLoader] - private void load() + [SetUpSteps] + public void SetUpSteps() { - Child = slider = new RoundedSliderBar + AddStep("create slider", () => Child = slider = new RoundedSliderBar { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Current = current, + Current = new BindableDouble(5) + { + Precision = 0.1, + MinValue = 0, + MaxValue = 15 + }, RelativeSizeAxes = Axes.X, Width = 0.4f - }; + }); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs index a5072b4e60..c3038ddb3d 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedSliderBar.cs @@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple); - private readonly BindableDouble current = new BindableDouble(5) - { - Precision = 0.1, - MinValue = 0, - MaxValue = 15 - }; - private ShearedSliderBar slider = null!; - [BackgroundDependencyLoader] - private void load() + [SetUpSteps] + public void SetUpSteps() { - Child = slider = new ShearedSliderBar + AddStep("create slider", () => Child = slider = new ShearedSliderBar { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Current = current, + Current = new BindableDouble(5) + { + Precision = 0.1, + MinValue = 0, + MaxValue = 15 + }, RelativeSizeAxes = Axes.X, Width = 0.4f - }; + }); } [Test] From 3b9c4c9d530eeee26f29a920299b0b363cd7348c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 12:04:27 +0200 Subject: [PATCH 369/896] Do not revert to default value when double-clicking disabled slider Closes https://github.com/ppy/osu/issues/25228. --- osu.Game/Graphics/UserInterface/RoundedSliderBar.cs | 6 +++++- osu.Game/Graphics/UserInterface/ShearedSliderBar.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs index e5976fe893..0981881ead 100644 --- a/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/RoundedSliderBar.cs @@ -98,7 +98,11 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, - OnDoubleClicked = () => Current.SetDefault(), + OnDoubleClicked = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }, }, }, hoverClickSounds = new HoverClickSounds() diff --git a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs index 9ef5f3073a..60a6670492 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSliderBar.cs @@ -101,7 +101,11 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, Current = { Value = true }, - OnDoubleClicked = () => Current.SetDefault(), + OnDoubleClicked = () => + { + if (!Current.Disabled) + Current.SetDefault(); + }, }, }, hoverClickSounds = new HoverClickSounds() From dbb69419e6d49e6661d31eb22f2445e499e5e742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 12:39:07 +0200 Subject: [PATCH 370/896] Add test coverage for parsing new online ID --- .../Beatmaps/Formats/LegacyScoreDecoderTest.cs | 14 ++++++++++++++ .../Replays/taiko-replay-with-new-online-id.osr | Bin 0 -> 1531 bytes 2 files changed, 14 insertions(+) create mode 100644 osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index c7fd3ba098..ab88be1511 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -101,6 +101,20 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeNewOnlineID() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-new-online-id.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(258)); + Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(-1)); + } + } + [TestCase(3, true)] [TestCase(6, false)] [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] diff --git a/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr b/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr new file mode 100644 index 0000000000000000000000000000000000000000..63e05f5fcdbda51e8f7c3322def87f4fc74f8da4 GIT binary patch literal 1531 zcmVvTrHZVD6F=Go4b8ul} zWo=<@Utx4?VRJGIAU8K+FfnCiFg9f~Ic8-uIb%6xWnyJxW-~W5FgY}2Hc0>o00001 z0000000e#v08sz|000003jk|tWgGt0+X(9f003P803ZOTO#lD@000007K}rXlJKRA z7M`&HOhZfu53FrP?1?N(Ce<_OIe53onYYKWQKjB!yy__GNTqRxRs4t|7d^=w?^a2; z=;p=RN*NtHSfdZ7KM{`CJ^MjsRn>7amAkGdpDK+x=PDo#tk4aBL?k%f)$Ebh0t%Nn zphxu;)QK_xDZ_`29#Slz$-BsTX6oS*tEpL9Ev7g9I5ScQ++9#K`PS+Bg7YLtZ zZhi6@^BMY%^-@>8#b8diGT>rK8W$eYWM@f+i3@MI*x2jwMoDv7i9dcH+Blva=xLAQ zQ+tf$l}n5c>1hLZYL!1qq`!()(I1Bi(teRO5k6sX_aTq*mVFs>aNFfP7QeOX;b~w1IAj)Te-(%eMdm^#T`gS4`h= z`(YRBu5x52OkrvAE;$$5Ka1rR+V@w)I!3+SveB0rxD~l$s=FEk0DPJXI01( zX<*T#vp20$0MeFEDmFU3$xU;pcASo(q2) zN#UlWQX+|aTArDrD&&we-Q#@5I>P@U#aZ7E_WVu}QZ z{u?U)bPE60zy8;&*@>H#nKF^riT4w%Ra_eUgCD(pFv%H+A(z{%SB~Z zD*JNyOplwu9x#_rabI#%4~0{9EZmm)IZ%cF!fh>HiAX@d}Izf*u8yw}?8+ zNnZ9YLWQcYNC*QlO6tVSziS_G!(qvFc@44`+vV{%9koFdUqvujoNp;)_jO1RN`{^0 zg=z?MuejP`10NKa5$Cxp-Se8FTJyl!tovh4wVKlspX4~-i8~TINGZ^#);*P#i)SE8 zzW1JsdM+A;?~>4&l@303`MAkXiv{_WIWc4$HYYGgMxnMFNGc{X2NX~R?2b@ZRs9cq z$!to>PZGzdmXRl&AW$UR-r`TGqkjmloZEFshw0Jkawz&1Ok4MmYz+~Jhh=obE&;f{ z+ZS(WELeZ_C2m3BkbxZ&DAUVh89w}bvS8+#qHjecQMK`PKw!@~opfX|DAuPffrNhq zDizxF1oWO4uo1ML<2%wPOFapwVrJdCB~~#Tt?D*NdN!;|NsC0|Ahbm z09^n8AOPI}000000000zf`AtsFAk^B)WLFNAyG7WV$N8-!j+hPu8$we_e;|#*+iNK z$kRN=q&Sl0(d`|*oEU1|*~5eG@dH3Miy0i*F=j=X+_a<6T3%2U@VpmXv8`-JhNX1! hacf^hACxf}U(1HP^l0fR;XoK|ePzq5Xz7uby&!FJ#7qDH literal 0 HcmV?d00001 From 8b9b085ef5422976b6ec9ef8cd11406c235209fc Mon Sep 17 00:00:00 2001 From: Termincc <112787855+Termincc@users.noreply.github.com> Date: Thu, 26 Oct 2023 22:15:10 +1000 Subject: [PATCH 371/896] Address mod incompatibilities Makes FreezeFrame and Transform mods incompatible. --- osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index 0a1aab9ef1..89a00d6c32 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Burn the notes into your memory."; //Alters the transforms of the approach circles, breaking the effects of these mods. - public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray(); public override ModType Type => ModType.Fun; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 2354cd50ae..92a499e735 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.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.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Fun; public override LocalisableString Description => "Everything rotates. EVERYTHING."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) }; + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray(); private float theta; From 238e8175ae3b2ddb8e8b86dc969ddba2225e693f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Oct 2023 21:26:26 +0900 Subject: [PATCH 372/896] Add ability to quick retry using Ctrl-R Matches osu!stable --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 20220d88cd..947cd5f54f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -149,6 +149,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene), new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry), + new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.QuickRetry), new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), From 526ee6e14070a2ba0d09ad544353be52796e698c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 14:46:24 +0200 Subject: [PATCH 373/896] Remove `IScoreInfo : IHasNamedFiles` inheritance --- osu.Game/Database/IHasRealmFiles.cs | 6 ++++-- osu.Game/Scoring/IScoreInfo.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs index 79ea719583..b301bb04de 100644 --- a/osu.Game/Database/IHasRealmFiles.cs +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -10,13 +10,15 @@ namespace osu.Game.Database /// /// A model that contains a list of files it is responsible for. /// - public interface IHasRealmFiles + public interface IHasRealmFiles : IHasNamedFiles { /// /// Available files in this model, with locally filenames. /// When performing lookups, consider using or to do case-insensitive lookups. /// - IList Files { get; } + new IList Files { get; } + + IEnumerable IHasNamedFiles.Files => Files; /// /// A combined hash representing the model, based on the files it contains. diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index 4083d57fa0..cde48c3be3 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -9,7 +9,7 @@ using osu.Game.Users; namespace osu.Game.Scoring { - public interface IScoreInfo : IHasOnlineID, IHasNamedFiles + public interface IScoreInfo : IHasOnlineID { IUser User { get; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 6b03e876c4..722d83cac8 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -187,7 +187,6 @@ namespace osu.Game.Scoring IRulesetInfo IScoreInfo.Ruleset => Ruleset; IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo; IUser IScoreInfo.User => User; - IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages From 900530080ff7f4c33a19b6107214c80ff4f0f997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 14:56:22 +0200 Subject: [PATCH 374/896] Make `SoloScoreInfo` implement `IScoreInfo` --- .../API/Requests/Responses/SoloScoreInfo.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 783522220b..0e31f11dc1 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -7,16 +7,16 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses { [Serializable] - public class SoloScoreInfo : IHasOnlineID + public class SoloScoreInfo : IScoreInfo { [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } @@ -138,6 +138,18 @@ namespace osu.Game.Online.API.Requests.Responses #endregion + #region IScoreInfo + + public long OnlineID => (long?)ID ?? -1; + + IUser IScoreInfo.User => User!; + DateTimeOffset IScoreInfo.Date => EndedAt; + long IScoreInfo.LegacyOnlineID => (long?)LegacyScoreId ?? -1; + IBeatmapInfo IScoreInfo.Beatmap => Beatmap!; + IRulesetInfo IScoreInfo.Ruleset => Beatmap!.Ruleset; + + #endregion + public override string ToString() => $"score_id: {ID} user_id: {UserID}"; /// @@ -223,7 +235,5 @@ namespace osu.Game.Online.API.Requests.Responses Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), }; - - public long OnlineID => (long?)ID ?? -1; } } From c3e9f5184f504f119867c3745af730c6fafe02de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 15:09:34 +0200 Subject: [PATCH 375/896] Fix `SoloScoreInfo` not copying over legacy score ID when converting to `ScoreInfo` --- osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 0e31f11dc1..ac2d8152b1 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -190,6 +190,7 @@ namespace osu.Game.Online.API.Requests.Responses var score = new ScoreInfo { OnlineID = OnlineID, + LegacyOnlineID = (long?)LegacyScoreId ?? -1, User = User ?? new APIUser { Id = UserID }, BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID }, Ruleset = new RulesetInfo { OnlineID = RulesetID }, From cbb2a0dd70ddce70e2e156be7bac632bbc9b6480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 15:09:59 +0200 Subject: [PATCH 376/896] Use both score ID types to deduplicate score on solo results screen --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index f187b8a302..da08a26a58 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Solo; @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineID).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); return getScoreRequest; } From 359ae3120494d0b9d3b61599d6f50f5abe0330ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 15:40:46 +0200 Subject: [PATCH 377/896] Fix catch distance snap grid not moving Regressed in https://github.com/ppy/osu/pull/25154. Specifically, in 013b5fa916d819ec7a8a93b1692d4aa027934a67 and 74b86349d58e5169e52bbdc70cef3dec81579a74. A simple case of too-much-code-deleted-itis. --- .../Edit/CatchHitObjectComposer.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 6f0ee260ab..4172720ada 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; @@ -179,5 +180,33 @@ namespace osu.Game.Rulesets.Catch.Edit return null; } } + + protected override void Update() + { + base.Update(); + + updateDistanceSnapGrid(); + } + + private void updateDistanceSnapGrid() + { + if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True) + { + distanceSnapGrid.Hide(); + return; + } + + var sourceHitObject = getDistanceSnapGridSourceHitObject(); + + if (sourceHitObject == null) + { + distanceSnapGrid.Hide(); + return; + } + + distanceSnapGrid.Show(); + distanceSnapGrid.StartTime = sourceHitObject.GetEndTime(); + distanceSnapGrid.StartX = sourceHitObject.EffectiveX; + } } } From 79910df9593b8478419b4eb2f4be12b0cfd1dbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 15:46:32 +0200 Subject: [PATCH 378/896] Fix catch distance snap provider not hiding slider properly Regressed in https://github.com/ppy/osu/pull/25171. The old code was kinda dependent on correct order of setting `Disabled`. `CatchHitObjectComposer` would disable distance spacing in its BDL, and then via the base `DistancedHitObjectComposer.LoadComplete()`, the slider would be faded out. The switch to composition broke that ordering. To fix, stop relying on ordering and just respond to changes as they come. That's what bindables are for. --- osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 0b1809e7d9..ddf539771d 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -98,12 +98,6 @@ namespace osu.Game.Rulesets.Edit } }); - if (DistanceSpacingMultiplier.Disabled) - { - distanceSpacingSlider.Hide(); - return; - } - DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing; DistanceSpacingMultiplier.BindValueChanged(multiplier => { @@ -116,6 +110,8 @@ namespace osu.Game.Rulesets.Edit editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; }, true); + DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true); + // Manual binding to handle enabling distance spacing when the slider is interacted with. distanceSpacingSlider.Current.BindValueChanged(spacing => { From 5d6a58d443cf6467bcc72b8db4d36875acbab7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 16:19:53 +0200 Subject: [PATCH 379/896] Add failing test scene for scroll handling in song select --- .../Navigation/TestSceneScreenNavigation.cs | 38 +++++++++++++++++++ osu.Game/Screens/Select/SongSelect.cs | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index fa1ebf5c56..a6d4fb0b52 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -7,6 +7,7 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,6 +17,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Configuration; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Leaderboards; @@ -34,6 +36,7 @@ using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; @@ -165,6 +168,41 @@ namespace osu.Game.Tests.Visual.Navigation ConfirmAtMainMenu(); } + [Test] + public void TestSongSelectScrollHandling() + { + TestPlaySongSelect songSelect = null; + double scrollPosition = 0; + + AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); + AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + 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); + + AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition()); + + AddStep("move to left side", () => InputManager.MoveMouseTo( + songSelect.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1))); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition)); + + AddRepeatStep("alt-scroll down", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(-1); + InputManager.ReleaseKey(Key.AltLeft); + }, 5); + AddAssert("game volume decreased", () => Game.Dependencies.Get().Get(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1)); + + AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single())); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition)); + + double getCarouselScrollPosition() => Game.ChildrenOfType>().Single().Current; + } + /// /// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding /// but should be handled *after* song select). diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d5ec94ad71..827884f971 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1019,7 +1019,7 @@ namespace osu.Game.Screens.Select /// /// Handles mouse interactions required when moving away from the carousel. /// - private partial class LeftSideInteractionContainer : Container + internal partial class LeftSideInteractionContainer : Container { private readonly Action? resetCarouselPosition; From 2fa221738184c9ded7f091cea16bf40f4e02765d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 16:26:31 +0200 Subject: [PATCH 380/896] Fix left side of carousel blocking volume adjust hotkeys Closes https://github.com/ppy/osu/issues/25234. A little ad-hoc, but probably fine...? --- osu.Game/Screens/Select/SongSelect.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 827884f971..dfea4e3794 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -1028,7 +1028,10 @@ namespace osu.Game.Screens.Select this.resetCarouselPosition = resetCarouselPosition; } - protected override bool OnScroll(ScrollEvent e) => true; + // we want to block plain scrolls on the left side so that they don't scroll the carousel, + // but also we *don't* want to handle scrolls when they're combined with keyboard modifiers + // as those will usually correspond to other interactions like adjusting volume. + protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed; protected override bool OnMouseDown(MouseDownEvent e) => true; From 0482c05d7c031132a17e5e0fa3e4b605051cd2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 19:27:05 +0200 Subject: [PATCH 381/896] Add failing test case --- .../TestSceneMasterGameplayClockContainer.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 393217f371..1368b42a3c 100644 --- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -108,6 +108,28 @@ namespace osu.Game.Tests.Gameplay AddAssert("gameplay clock time = 10000", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(10000).Within(10f)); } + [Test] + public void TestStopUsingBeatmapClock() + { + ClockBackedTestWorkingBeatmap working = null; + MasterGameplayClockContainer gameplayClockContainer = null; + BindableDouble frequencyAdjustment = new BindableDouble(2); + + AddStep("create container", () => + { + working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio); + Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0); + + gameplayClockContainer.Reset(startClock: true); + }); + + AddStep("apply frequency adjustment", () => gameplayClockContainer.AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment)); + AddAssert("track frequency changed", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(2)); + + AddStep("stop using beatmap clock", () => gameplayClockContainer.StopUsingBeatmapClock()); + AddAssert("frequency adjustment unapplied", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(1)); + } + protected override void Dispose(bool isDisposing) { localConfig?.Dispose(); From 565ae99e0dfddbecef6bc24828ba6c39c837f5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 19:27:50 +0200 Subject: [PATCH 382/896] Fix `StopUsingBeatmapClock()` applying adjustments to track it was supposed to stop using - Closes https://github.com/ppy/osu/issues/25248 - Possibly also closes https://github.com/ppy/osu/issues/20475 Regressed in e33486a766044c17c2f254f5e8df6d72b29c341e. `StopUsingBeatmapClock()` intends to, as the name says, stop operating on the working beatmap clock to yield its usage to other components on exit. As part of that it tries to unapply audio adjustments so that other screens can apply theirs freely instead. However, the aforementioned commit introduced a bug in this. Previously to it, `track` was an alias for the `SourceClock`, which could be mutated in an indirect way via `ChangeSource()` calls. The aforementioned commit made `track` a `readonly` field, initialised in constructor, which would _never_ change value. In particular, it would _always_ be the beatmap track, which meant that `StopUsingBeatmapClock()` would remove the adjustments from the beatmap track, but then at the end of the method, _apply them onto that same track again_. This was only saved by the fact that clock adjustments are removed again on disposal of the `MasterGameplayClockContainer()`. This - due to async disposal pressure - could explain infrequently reported cases wherein the track would just continue to speed up ad infinitum. To fix, fully substitute the beatmap track for a virtual track at the point of calling `StopUsingBeatmapClock()`. --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 70d9ecd3e7..6e07b01b5f 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; - private readonly Track track; + private Track track; private readonly double skipTargetTime; @@ -188,11 +188,11 @@ namespace osu.Game.Screens.Play { removeSourceClockAdjustments(); - var virtualTrack = new TrackVirtual(beatmap.Track.Length); - virtualTrack.Seek(CurrentTime); + track = new TrackVirtual(beatmap.Track.Length); + track.Seek(CurrentTime); if (IsRunning) - virtualTrack.Start(); - ChangeSource(virtualTrack); + track.Start(); + ChangeSource(track); addSourceClockAdjustments(); } From fdb81bfa4ca2d9819bb0c252059b7b84919af583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 26 Oct 2023 19:38:41 +0200 Subject: [PATCH 383/896] Rename methods to not mention "source clock" anymore --- .../Screens/Play/MasterGameplayClockContainer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 6e07b01b5f..1c860e9d4b 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -145,7 +145,7 @@ namespace osu.Game.Screens.Play protected override void StartGameplayClock() { - addSourceClockAdjustments(); + addAdjustmentsToTrack(); base.StartGameplayClock(); @@ -186,7 +186,7 @@ namespace osu.Game.Screens.Play /// public void StopUsingBeatmapClock() { - removeSourceClockAdjustments(); + removeAdjustmentsFromTrack(); track = new TrackVirtual(beatmap.Track.Length); track.Seek(CurrentTime); @@ -194,12 +194,12 @@ namespace osu.Game.Screens.Play track.Start(); ChangeSource(track); - addSourceClockAdjustments(); + addAdjustmentsToTrack(); } private bool speedAdjustmentsApplied; - private void addSourceClockAdjustments() + private void addAdjustmentsToTrack() { if (speedAdjustmentsApplied) return; @@ -213,7 +213,7 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = true; } - private void removeSourceClockAdjustments() + private void removeAdjustmentsFromTrack() { if (!speedAdjustmentsApplied) return; @@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - removeSourceClockAdjustments(); + removeAdjustmentsFromTrack(); } ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo; From 24b1d1e9558badb2f1a6cb5dc30a5aa6fc79f41d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Oct 2023 18:18:07 +0900 Subject: [PATCH 384/896] Fix code quality fail --- osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs index 89a00d6c32..f1197ce0cd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override LocalisableString Description => "Burn the notes into your memory."; //Alters the transforms of the approach circles, breaking the effects of these mods. - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray(); public override ModType Type => ModType.Fun; From f931f4c324f96063a3964fc660daf06d99e657d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Oct 2023 18:19:44 +0900 Subject: [PATCH 385/896] Remove reundant interface specification --- osu.Game/Skinning/SkinInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index c2b80b7ead..9763d3b57e 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -14,7 +14,7 @@ namespace osu.Game.Skinning { [MapTo("Skin")] [JsonObject(MemberSerialization.OptIn)] - public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles + public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete { internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD"); internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0"); From 7140eee870bab0d86ab25c15f3d56dce1b68d627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 11:38:10 +0200 Subject: [PATCH 386/896] Add failing test coverage for quick retry after completion not changing rank --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index a6d4fb0b52..2f378917e6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -259,6 +259,7 @@ namespace osu.Game.Tests.Visual.Navigation var getOriginalPlayer = playToCompletion(); AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action()); + AddAssert("original play isn't failed", () => getOriginalPlayer().Score.ScoreInfo.Rank, () => Is.Not.EqualTo(ScoreRank.F)); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); } From 86a8ab6db6548d211e5f0e5ff1a4feb4b7a506c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 11:39:43 +0200 Subject: [PATCH 387/896] Fix quick retry immediately after completion marking score as failed Closes https://github.com/ppy/osu/issues/25247. The scenario involved here is as follows: 1. User completes beatmap by hitting all notes. 2. `checkScoreCompleted()` determines completion, and calls `progressToResults(withDelay: true)`. 3. `progressToResults()` schedules `resultsDisplayDelegate`, which includes a call to `prepareAndImportScoreAsync()`, a second in the future. 4. User presses quick retry hotkey. This calls `Player.Restart(quickRestart: true)`, which invokes `Player.RestartRequested`, which in turn calls `PlayerLoader.restartRequested(true)`, which in turn causes `PlayerLoader` to make itself current, which means that `Player.OnExiting()` will get called. 5. `Player.OnExiting()` sees that `prepareScoreForDisplayTask` is null (because `prepareAndImportScoreAsync()` - which sets it - is scheduled to happen in the future), and as such assumes that the score did not complete. Thus, it marks the score as failed. 6. `Player.Restart()` after invoking `RestartRequested` calls `PerformExit(false)`, which then will unconditionally call `prepareAndImportScoreAsync()`. But the score has already been marked as failed. The flow above can be described as "convoluted", but I'm not sure I have it in me right now to try and refactor it again. Therefore, to fix, switch the `prepareScoreForDisplayTask` null check in `Player.OnExiting()` to check `GameplayState.HasPassed` instead, as it is not susceptible to the same out-of-order read issue. --- osu.Game/Screens/Play/Player.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 97bfa35d49..18d0a65d7a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1115,8 +1115,7 @@ namespace osu.Game.Screens.Play if (!GameplayState.HasPassed && !GameplayState.HasFailed) GameplayState.HasQuit = true; - // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. - if (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null) + if (!GameplayState.HasPassed && DrawableRuleset.ReplayScore == null) ScoreProcessor.FailScore(Score.ScoreInfo); } From 3944b045ed4582b233066dc714be6075b47482f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 11:58:02 +0200 Subject: [PATCH 388/896] Add extra test coverage for marking score as failed --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 2f378917e6..7fa4f8c836 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -247,6 +247,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState)); AddStep("attempt to retry", () => player.ChildrenOfType().First().Action()); + AddAssert("old player score marked failed", () => player.Score.ScoreInfo.Rank, () => Is.EqualTo(ScoreRank.F)); AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player); AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); From 96d784e06bd6068f09620eaaa45c4766d0da900d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 12:39:54 +0200 Subject: [PATCH 389/896] Delete `ScoreInfo.HasReplay` as no longer needed --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs | 2 +- osu.Game/Scoring/IScoreInfo.cs | 2 -- osu.Game/Scoring/ScoreInfo.cs | 2 -- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 2 +- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 6ccf73d8ff..5b32f380b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay OnlineID = hasOnlineId ? online_score_id : 0, Ruleset = new OsuRuleset().RulesetInfo, BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(), - Hash = replayAvailable ? "online" : string.Empty, + HasOnlineReplay = replayAvailable, User = new APIUser { Id = 39828, diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index cde48c3be3..a1d076b8c2 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -22,8 +22,6 @@ namespace osu.Game.Scoring double Accuracy { get; } - bool HasReplay { get; } - long LegacyOnlineID { get; } DateTimeOffset Date { get; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 722d83cac8..d712702331 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -94,8 +94,6 @@ namespace osu.Game.Scoring public double Accuracy { get; set; } - public bool HasReplay => !string.IsNullOrEmpty(Hash) || HasOnlineReplay; - [Ignored] public bool HasOnlineReplay { get; set; } diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index b6166e97f6..df5f9c7a8a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking if (State.Value == DownloadState.LocallyAvailable) return ReplayAvailability.Local; - if (Score.Value?.HasReplay == true) + if (Score.Value?.HasOnlineReplay == true) return ReplayAvailability.Online; return ReplayAvailability.NotAvailable; From 32fc19ea0d2515aeab4e84248f6cf67ec7d7de98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 13:22:17 +0200 Subject: [PATCH 390/896] Fix results screen test failure --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 146482e6fb..ab2e867255 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -362,7 +362,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; - score.Hash = $"test{i}"; + score.HasOnlineReplay = true; scores.Add(score); } From 2d5b1711f6ffc233ca308153a665d18dcbbccea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 13:27:48 +0200 Subject: [PATCH 391/896] Share `!HasPassed` condition --- osu.Game/Screens/Play/Player.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 18d0a65d7a..a1ec0b3167 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1110,12 +1110,12 @@ namespace osu.Game.Screens.Play failAnimationContainer?.Stop(); PauseOverlay?.StopAllSamples(); - if (LoadedBeatmapSuccessfully) + if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed) { - if (!GameplayState.HasPassed && !GameplayState.HasFailed) + if (!GameplayState.HasFailed) GameplayState.HasQuit = true; - if (!GameplayState.HasPassed && DrawableRuleset.ReplayScore == null) + if (DrawableRuleset.ReplayScore == null) ScoreProcessor.FailScore(Score.ScoreInfo); } From dc7f5cd6edc94275a56b3b5cdb5766c76c84181c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 13:30:51 +0200 Subject: [PATCH 392/896] Add preventive assertions concerning submission flow state --- osu.Game/Screens/Play/Player.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a1ec0b3167..58c8c3389a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1112,6 +1112,8 @@ namespace osu.Game.Screens.Play if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed) { + Debug.Assert(resultsDisplayDelegate == null && prepareScoreForDisplayTask == null); + if (!GameplayState.HasFailed) GameplayState.HasQuit = true; From 6789a522d6d79d6fd1d4f8c15b9c83b119ab61b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 14:15:30 +0200 Subject: [PATCH 393/896] Rename test to distinguish it from test-to-come --- .../Visual/Navigation/TestSceneSkinEditorNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 88904bf85b..d08d53f747 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Navigation private SkinEditor skinEditor => Game.ChildrenOfType().FirstOrDefault(); [Test] - public void TestEditComponentDuringGameplay() + public void TestEditComponentFromGameplayScene() { advanceToSongSelect(); openSkinEditor(); From b5cb5380045b0d8be6031cbdffb4a799ac402bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 14:23:41 +0200 Subject: [PATCH 394/896] Add failing test case for skin editor freeze --- .../TestSceneSkinEditorNavigation.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index d08d53f747..c17a9ddf5f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -18,6 +19,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osu.Game.Skinning; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; @@ -69,6 +71,28 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); } + [Test] + public void TestMutateProtectedSkinDuringGameplay() + { + advanceToSongSelect(); + AddStep("set default skin", () => Game.Dependencies.Get().CurrentSkinInfo.SetDefault()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() }); + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + + openSkinEditor(); + AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get().CurrentSkin.Value.SkinInfo.Value.Protected); + } + [Test] public void TestComponentsDeselectedOnSkinEditorHide() { From 5ad962070c0448b81c8457b8816818b6e6041502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 14:34:30 +0200 Subject: [PATCH 395/896] Fix skin editor freezing game if opened during active gameplay --- osu.Game/Database/ImportParameters.cs | 6 ++++++ osu.Game/Database/RealmArchiveModelImporter.cs | 6 +++--- osu.Game/Skinning/SkinManager.cs | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/ImportParameters.cs b/osu.Game/Database/ImportParameters.cs index 83ca0ac694..8d37597afc 100644 --- a/osu.Game/Database/ImportParameters.cs +++ b/osu.Game/Database/ImportParameters.cs @@ -21,5 +21,11 @@ namespace osu.Game.Database /// Whether this import should use hard links rather than file copy operations if available. /// public bool PreferHardLinks { get; set; } + + /// + /// If set to , this import will not respect . + /// This is useful for cases where an import must complete even if gameplay is in progress. + /// + public bool ImportImmediately { get; set; } } } diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 730465e1b0..5383040eb4 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -261,7 +261,7 @@ namespace osu.Game.Database /// An optional cancellation token. public virtual Live? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm => { - pauseIfNecessary(cancellationToken); + pauseIfNecessary(parameters, cancellationToken); TModel? existing; @@ -560,9 +560,9 @@ namespace osu.Game.Database /// Whether to perform deletion. protected virtual bool ShouldDeleteArchive(string path) => false; - private void pauseIfNecessary(CancellationToken cancellationToken) + private void pauseIfNecessary(ImportParameters importParameters, CancellationToken cancellationToken) { - if (!PauseImports) + if (!PauseImports || importParameters.ImportImmediately) return; Logger.Log($@"{GetType().Name} is being paused."); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ca46d3af0c..59c2a8bca0 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -182,7 +182,10 @@ namespace osu.Game.Skinning Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)") }; - var result = skinImporter.ImportModel(skinInfo); + var result = skinImporter.ImportModel(skinInfo, parameters: new ImportParameters + { + ImportImmediately = true // to avoid possible deadlocks when editing skin during gameplay. + }); if (result != null) { From 35f30d6135b29b73b68d861bb68564cc951f1a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 14:39:58 +0200 Subject: [PATCH 396/896] Scale back debug assertion The import preparation task can actually be non-null when exiting even if the player hasn't passed: - fail beatmap - click import button to import the failed replay --- osu.Game/Screens/Play/Player.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 58c8c3389a..2392fd9f76 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1112,7 +1112,7 @@ namespace osu.Game.Screens.Play if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed) { - Debug.Assert(resultsDisplayDelegate == null && prepareScoreForDisplayTask == null); + Debug.Assert(resultsDisplayDelegate == null); if (!GameplayState.HasFailed) GameplayState.HasQuit = true; From 7a5f3b856f41d3288b2d7940cb9009dabf8d8770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 19:43:49 +0200 Subject: [PATCH 397/896] Add visual test coverage for displaying hitcircles with high combo index --- .../Resources/special-skin/display-0@2x.png | Bin 0 -> 5085 bytes .../Resources/special-skin/display-1@2x.png | Bin 0 -> 1262 bytes .../Resources/special-skin/display-2@2x.png | Bin 0 -> 3534 bytes .../Resources/special-skin/display-3@2x.png | Bin 0 -> 3456 bytes .../Resources/special-skin/display-4@2x.png | Bin 0 -> 3213 bytes .../Resources/special-skin/display-5@2x.png | Bin 0 -> 3647 bytes .../Resources/special-skin/display-6@2x.png | Bin 0 -> 4954 bytes .../Resources/special-skin/display-7@2x.png | Bin 0 -> 2503 bytes .../Resources/special-skin/display-8@2x.png | Bin 0 -> 5710 bytes .../Resources/special-skin/display-9@2x.png | Bin 0 -> 5195 bytes .../Resources/special-skin/skin.ini | 1 + .../TestSceneHitCircle.cs | 10 ++++++---- 12 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-4@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-5@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-6@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-7@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-8@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..67d2e2cf04a71c59957a79a69ef6de8e02a6bbb2 GIT binary patch literal 5085 zcmV<36C&)1P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY6LLvJK~#8N?VZ_^ z6-5?@3lZ!_pk-?|*&7g0K^O!?15pPaa2p~fG=h2b#Xn*Gf_a;HG$z7VA|zsh2?xXl zM-~MFK?D`q5m|(85Rk0_Mdy5Z>$G{Zvg+2_Ko7r&jI64AZ!KS)I(c%byum%)Q;$%u zFuyIDIdkS>$yy~fnGNOZx}=Uhd-iOot==AWD`X4ma7p7OwMiN*saaAL>XdX|(qT!* zB(+QGcIe##S>V1V*(^yjBmtE)sBhoCBP0)g`|UTf*!J(P(f2i1_36{+jAR!i z0i3i~(r$shO7hB88RXDvr%0MAX||+^O-)T>BsWrUjX)Iz>^n|#bMpyVJSu6Iqyv%; zR%zX(Ll(I6BzsoUA0$ndG+f{si&foDz;@oRUq37y7jj6_PDvXD_IAljRjH6exA20b zg<>t!*p~IeHLn`5`~3$Wd~ly+TO@s4wi_u0vSq!0lr%#C*ZEpinhPH^XizXO1$dbNo)UKF`0(MukRe0N zHkQd{f9AUi(gp1TXT7ttGdO?#ya9aT#EIbh@4q*>0zZEIc=+v_e1J87=%I%W%Hn&H z3WLrKIS*jqjT$v7m@;KbFlNjclN7K*MG7zrU_0k`*REYNV9%U6V_=i^@82JsI&~^I zeE4u8V9mvKl>)l{Y*Q*LVEim_r%#_AOrAVBm^yW;0WO72EU+Sgk^Mb@W8r)E?hOtc zI1qgG)mH{K%g6^*V8Ma~!BbB?W!8rc8x}}KBobAwI| zISXK-11xvx(xt&;k3AN#+&ExW0=5NMo-)b=&h^%> z1zD^Z3;weJp4U?Vs{oCE1~hI1(1d(0V9l>wxzZ{zd@s4y8v>emx}j1*4y|~xJZ3Gd z_Y}Y?_62A{Nd--<(FLrzK<*>iS%Lgtv!Q_+LJqC?Pg24@&U#M+tYV))R?t8H{Id!3 ze4xdeTLknix$e$1*4z+sXvK>p3~KQKCSstFyPE;4*r$R9a-2mYfegD;)|?30wqhS( zN@&6~qKLa4uq_2mV-ppbj*gBx!07Vj%Pn&KsjRtn*72e@W1+!5%z#Fxa_sXRvMCw&1hRJ_~m3+7;~Cv!|>5fzN4Gqj17+{`AvN=J(Vn z3CqfmxJlzKXvw2-$+c)CkQ@4D@7ahI(?9pJSn=H2mU_U%lvpus5fprJz;rzgV45lM zeZbthb!+hHr=JF&fBv~?%wZcqfeBQWv2LvF=+UF5Rqea)zB3jl=WL#`vOr_awB~UW ziJx7iv1ad?$PboE_JUZk>EW;eBdk~(dy2M202j;s^2;xSEnBt(8#it=P`~)%3-gEq zPLJza(#4Ax&2IsWE8)9rqxL2hr&6pMYvvrW=6(Cr^QHk!wOP+2ujsu|-<)907lhjx zIX2@v>J)(-x{77;*v)YPlee|C87p>uu_`Oz_3PIMn>KAS{uj8&KAxScblrD;6o|MI zx}pG0D2Dqe0vZ=YS<%3n&q=mdAV*wA1hTpcIjP9N6Yl8TxpRYOo_VIu7pu|&Mpzpb z3}6Z~`#8Q_rRzSxc8>3+GC}Q)bL9zi&o_(gNW*8%0{N^%U~3SQAJLi z6Qy|&%WzLv*V6#TiYcvk?AVbSu*BhKB<+)$7Rkg@N#93Wh;HA$-4tF$gxh)a(ML_A ze~bo6@_txn$?7V^4b1Ufz$1@5VtlcCpsav#6Zl{XJ1hhFj)w&72|1tLvPd`m$C4O? za#9MloAMawavX&9R7Fa2phiMMOkJ*%Tw- z7*}h{h0L#;eHTvG{kU=Cy1MSO0%je*NKH+xz;_%hSQc>$Z~Nf73cvHVr2i;rU!jO^ z#{Xl9MMQZ-ijmhZyO8;U*@Jz8?39r`fJS)$BPtiT2-+U({oA>SM~(4lsk`69?*e;+ zJZZ6oA~%Gpp9CddxGL?j6bu(psav#6%;~* zKRy6RtQa4%K8*#t3PJM(?2X$$AsCR0xD3iBAUALs40P|3ZOMEv*gY{GJ*uY?Hsu;R zrMV81(oo7hPUt4+oZM(OlrrWNTH{`4anL? zkq0o664zWH0{~qBp%~7S$xXzFo3x{~we_s;I8;)Kx{L;XIU*afkqa0Rvf(Q70>)L~ z4}Gozg;u;PkDG86-lVOPObKTn2WiWlR^js~$uIj9fsAax#FdP=!6&1vfVnZa2q3$z z!V-~vZD3kCV*J{1_c*M}O)yUs7M)iq{BoTlkZr)oSLkH{jEE|@Fp}@WMT}_$OeqqO zy#H}n&f7#9$UZJ(-$+Ho)2%MFk-V0y;>5JjlSkTY%L#PkY+3s++f}H%E&~Br+e|7@q(b6_ ztS3h90@PgPZg&Cur27wDQ>m^(r7mOS$dO$xLq%5-uB%Al7bjnZBm&t6?5b?Q;@7Hj z6)M_5BF?vr!mokqDkKdpS(O(sy}ehiTrrQR4LB44BHcA5iY{zg>mn*z_)%8TDi@== z3P~zUR)DIsfDwhB-DR(U!~FxG_CYnGAn-yiTKHwDkfgR`RTjXA@Qqp?jY1CK?ofxM zVr`o-%2FXoWyz|nfDsiCN^%!)cYsbR{E8||g(S5l=K+ie-}JHc?3YKmKDH}g-omd8 zrLB;pvgEvgZMpjma~@?L=_(al_;r^OE!kFH!1niSz*1;b9lqT$inU~0MJ!ll0}fSn z_;$z0Yso4rp3Mzde}{ZG@c%)vkZr)KJQl3-0QPn|_C$Fh+u>Fb7ZE4+E0q*)r(;i4 z1aj3C2HkIXoJgl*Pn6e^6+{`Ji6b?ZbO57Br(*@8wqW`$ZWC943zW1(*n(0N z`ePvL>v0t*((n}vSwRq@&_}03tn!G*k}!_>h#bJ$g+5yzVcz8~Q zKXNhQ)_hWH=Ax4nTJ(#O-V~UBvthH$Hd%aA(u?7DW3_0}qF~{|g@vscS3$ku?Af#7 zmWU#S9;$nRy}iBt*oYA$<~BDsPmt2nyb!Lyi8?5ic}a)a0*Z*?Oxscld6cm;eRH&p-dXiExVO$dMz#=FOXfk3M3$`>5nR z@ge_(V{J=KzDY%Jn9~G6*PE#t*733=ul_RkkiIyXN3`YA#tdZU3&O`GNkFz(u%`*! zX>6-~8ZQ7dfAdQ(y;OJ_2E@cu%N2ifyB2wwomiQ|s|;&K3L5tzCu(kD-ollLg#vELk|o9mmjxIr1~Btw_|7fKRiHS? z#$|{l8<*jN#)7eCB7~yWi~voejsC|7iT$R94P2(}(E<+8MGHAwSTVDH0otb|SAk+X z@4^siMMp=6X=rglzxwK{!HN|t%*7qM_%NS6&HTfBp5+FAQ;U)UJRW z*G@o}7Le^B2W5--l2)!<8LVEtI^0q(0ZfU#QUEhmk5`G^&}z-{7>uz2Bi5HMUv8}V z>8GC#xArViyIZwtm2nd>)o37Zl8m{MVrMvTYkaPlD(t8aFye9MP8Xd&;!YG#5-~9o zL?r;mink>JM&5!?B^JC_3c;yT@{iNt-v%z9o2kHv3@Etpkv&BecI(!y3*LYK{cuZE zQs7~PYvEMe;#VU}M#h?1a4i~9&15=A3aXwWo=l!lnt0=s){0T0B_p3TyJ9A8hGS6x zQ4rnD0`QtOYs}<;SaY1jiurI|BP&J?AtQl2RI>2`+1qr6+h8ab|7bwP;%)`4fH!Q| zVEl2M#EO~qYqeOhxANgC739#GN6OtBEug)t`aV ze~A@;SliNAsUdUM1T-yRW8&Nf!gVn9Ikhy>o$NlaUM!a=f>15V2P3g!{O`Zhx(QWk z$jEXVBL#HgNK9JP?o{_-TQ3z2a*QKM0+`ulK9EF9TQ&d3rwZLhvw$8VpcCiGRrf(o zTQrt^J7Ftaz|!DDTLca9jT8dT{TH5@A%|{*$$R>y0gZ()wJu{7Xqd-e(?>=mLPe%K zfV*&MvShiD^`fGHQSKr1Gk@=jUvm2N=^18Y3d!%xFewHgQcI+{ly=`T8+m}LjZ556 zZZ0>PiA~~K(tIidFv<-Xg`m6sd0k+8NAASQ1x~`9pfoLPHcV})(9D&4uBtM~DhoCd2rtTz24yWI%aDMj-GR7`sVgbiDut`69I{GaGbEN;B5x%z zmY_`%z_q)ZyfFL_B%>?Mz{yZ7Gj8~>Bqm=yC~&)N?xSvl?8<_!wGrB?eel!D2FU;u zdvqnCM@M&pwRPJd$H@aft!^vGd%CBdt|0gyKx8$ajM?KN00000NkvXXu0mjfQ7V3M literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2df10655efabdb9eefbad32abd4be42deef4fb02 GIT binary patch literal 1262 zcmeAS@N?(olHy`uVBq!ia0vp^IzXJo!3HFcw{9)~QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0XBc7rhE@gG;rfNGjOT^vIy;@-}&_Lp`PXb?Yi*oMiei6eZX zfU1z7(2@mSQC`1P_AA)8sp&2}q+9ADa#Dn?(LsUB;Y2`!gFxIQQK=JW?)7fIcf0!C z+nap9Jtu!%`@YQnSn+i03$r45E#T>A1DjarutZbLY;Te{PxcT!RyJ zZO@dCM0Z@jaos^q!A~K~@t5^`#;ikmY5SiP?ezEKPxkls-&0#x$Cs07uar}t)w!Uu zs=9jmgQrTVyJY_t9yB^|rf8PqHP*QodX>47&wet>5R2UIy8PBp6|Ul0KG&Kf4p$ti zef#RwEFu0^FJ2@#J3HqjBs6?)y?E`}&JQe??!9~R=G(VvckkTU@cQ*@tDF1w?bFQ= zdvWA5M|oM9-$OH56CyP%1t=SibYQ|{=?Z{M`((6s5(Zyr0wcHgbw9}rk2`tx>gYPtU; z^W-Xl=i(nZl(jMsh?(vgRw^U5fQT9IZw=nhS-=x$TCxXIM zJj4RIy4it_X}L2$WLlr|pA^CVf(!l`ULYyXvj!7e*YtSV1+^XR=gyp>BV>8FMKkha z#jNns&f2Hja!N|R$eO%QTyB$h+41h(yF7W8H}Bq+-5+jiVXG2z>dZZ7&-#Yxh(#_8c(DHb z`SU;j?dsU1#gsi`0{g_QQBNo6Yb>nm|RUf(A7WbUDqjcdbJzxnuaV*YETetl)G zcqa?d_d>4I3<{?T99Pde6fpnC>II=Ylyn^bG~H;@X6cX7JHB}N^5r~%NB@R4FLj)t z6BK^<-Q<@_N6b4|UH>%fFj4hp@vQcTa4~-2&K=+=#)~E3KRos__o(YBFc2m zR-H)}hnM}Y{-!)}gV(mytR1RGYbI@#{HC~-C;jN@vm4$S$A)Y%o?{v%DVF(bHOoaI zQy-H>I&pct>8)kF>Aj+6Bq855_J1#&y5qI<&~zbTV`Jl-va)IV>V917w#7zACj&D` zUca;5{BV8YzNYyfZrqTNmXo^{CD!=brQqk$j~_SQew^TPLuJ#4`ua;BmkOUdedfT= zf;hjJ#PgRrdBDs!_5!<0=N!F0iKYIjhm3<&w|-q<-OBExIz4|`-`mdZZe_Mq1eQ?@ Mp00i_>zopr0AYwh{{R30 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-2@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb8ec0edf1953c6a8b036e6e33207bb99f84bf6 GIT binary patch literal 3534 zcmV;<4KebGP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY4P!|}K~#8N?VZqP>6+T%?4K zFmOSJm?FrfUJw{;FwNL>^822h-}+2BGqW?h>mBZXq@%NQJiGRM_?vkKi_-rn{g^75 zT7gF7{i1#Q_8UKc{v0bb`10k8ubkocd!3)`-_x>X%g2;Gp>&^8N@O-OavKE&1rsQn zlarIvH&znE>tZ-(8cQDXj~_p_{qW&K0j1&=En4)W;u1ip!%lWX$i5V#2( zOWE|?+}y%giT)z2In$W(tT{=H%cP({lhl{4v5?Y768TV`7yvhcBPpA}0^R;NHD@wO zo(hiNzklEU?c29SbZtGTs0&?d>!8u-)&`;j`7SRnZ?yWyeERfBbm-7Qv}@N+mL*D) zCY~L?#P50U-n|pAUcC|zA3pRY52(_qjYm5Pd50;Tp!V?x{LC+&0^7E2I~vFQ%Sosh zGiHn!GGvJDBAYgCYP^cUKewKzlcPJ|?Q>thek~q9ek>k5cpyc-c=1B!_wS?MXuwIH znp03f;(sXZNkpRal@O{x^_TVR*;6cCx>PJ&xKNfc){Ezhww80w1U;W~s*2*-vu9Fb zO-+q_U0GQvZrr#bqw?0RTb}Yb@7S^9kCc5+$+tw$HjsKRQyYk?TT)UY1`HS=kAd}~ z(j^b?qrXo!DLs1hkbe_W|Ni|&mo8l(M6_zvN}lud=~MY0ojgGQ82NO5KE2GL>wn>= zbMX{NJyxOm%c#u|g9i_e3v{|DU@hmI33|TneGFvb9~m{JZ{NP6YuBz)WScf^bl##EO6k!fwD|@QP!LZdcN*`UO*Z;zFxh0;hN-YnxV*_q0I!U zJb+`8*p;HT3BBi?+YrZ7ApHTh2t*I8MqsAOQ)$+$nQSY&ckeFSM~H_wIg7@Dru9I~DaVZ)XKO2fP8Q2ybmuYFa;IZV$8f=zS)dKWB0@62V#D_B z+r`0y2Sr6ig;Cl74Zty={yY6_500I$fKUah5tx&cBL|_n6=|Im9agzIte=~kn=1$P zSh~U(4v~X_F{Wr?fxKP2c8McLju<5kkid(S{fz|v3&)-UX~1cUKy^F_RJm?BW9`+w z&gWy@`ydZhZqlSlV#0(8vNOOSISkjXT@zckZWY_MZ8J(5unP2?%29;G=fasL4|#QU zwYYinrkvMe@e~WISo;eDYT3Pe_vEbsn!XvOG4k^Au=?-=mCO3=R)4TP;_PChX2aeZl|@o!Vd$+07~s$C%eHS+Yb*#4sl)NfVf9&_5_vM3z$erlzK*n^QGk zzka>gv}u#$Sk*6n1^OF=Wf1g-V*vYaN_TZ_BnHELza!N|PM`rs2=8d%{MITDIaZjF z(m~_|QWeIwyJ?h)XbdRZWvfI?^ZfLn$ske!jrQ-ClopUYtk2oxA?N!Y=^!ngND_K- zwFfRHd2%S@Bo9kb*t@f4%^E2(D2?6=8`7}&i1jn<1HqUAW4R#6F(3_)3!J2>`O1|m zSk6X;{JS_U6qSQ^X`X!|*zv^Xb#4i)G7}$(>_iq7Urbw@>Wcxl>LN zRZ=pHLBmezkM#1llzu~L9Z6e4X+*l@VKrvz)TuHLD-U}NvET6J%a=xpp3nkG95?Kn z>gwvgr4*OO_?5xClGa%&7?G!QYp!^^@@yD9&Q)n-UaSqU~><0zqbYW zm~)87I1itZEOE@~)2GFb9XrI{y?c!kGbHc;Wq&1s@?jm{0u6bHym=(96lH$o6&DxF zO^(yLXHI`}d2B7tz_8A$i!*zl-GkAy$%D8;HpBEf0H6 zX3w52hZGppu3NWGEMB}=-gkw-Op^z$UcD--t&=eZ{oj2Sbev}E0SOa+KO2*kY*7x3%^cJjyZ6lhdkbQqrU=v*la;#RF%6$x>SSM*W! zeL&eYZu=Nlfl9kK%dNxc)2EM^J9n^kFl<5Rar^ddqr{Orl>Qrg z*??CBa=X`GCl75MB5nEdsKa;Kq_JPe9abs-S5iztzZ zJc>T~7_2V(fK8wpeNOT)&cpV$`Sa&Ts_J5#hn{xNo;~8&v15MZA@cC}>j_z)!6^b| z^f}1`n2I6#q9hMdcj(X|xn&LVJUR@P=j*Xo(_EOM4=c*Q$6{!#BnHcaG|VtCNJfvF z$sppet_OLSE?p9rFJHE;E<2HkJYbDL-1sr=hhTYF7~n;Lblvd^0#{Wm3Sf^PqVB|r z6Dg4gtP%JfWxu8fG%b$ustTeHb6ZT$8XJx<>_Xt1=-AJPmE`N!ugf{D%c6jdAo74! z;9n@i{fzjujo0(AvVu7-W*y1gkBpUHRe~Q_6cXNx$Nm961NSjVfIRjK$qV#-2_P;| zZU3zDuo#MMV_1?(R`j85LSv5YwAcm868sQUlob*e>Ki+NDiFh$-__RE%IBR}9%SMP z9#maS*^&``JQhP+b@Jp%qcpvcyz7)Re1ngUD0tQQyvDg zjUkW3J)m@yUgEy)<}mVrZ5(Koha3WvG2>9>`Itf_4OCT16_jv4V+W;wlDIzysj^m_ z2GS;Zcx(VSfx@QOJ&d>!{Q-)pQ_zu8CP`@(}>+;xEkn%5LbQGvYAht_v z*swv40mFdE!@w3*SO>|&k}w+fa!TI&D$Jx@mJ>FCG(a^)V6p`2b~`a7FVH}?v2kq! zK_WW_f?`Bo~l$_z^Fj~(0KZKt2GjBZJ>274AL=_i2!(F6)W~8T*rZ` z#IRsfB)+>Z!r+g$J?Ekw2PQiLv6B=#O20O2ZD4hEwP`MlrTmQ>H;Q@l=E<_4;2uVK zdAVGP!7@I+zBm)XN;jT(zjFuMVH%(|(DZx=V-r|aRb`qBCrco3x)GFVT)1#Sl$Di< z^XJbSB{32bs7BzNIdhUGFbep6#EbFO2KxPSoS!4Ey>J$9v=OM19S7=ht()0M^hPT% zE6*R8VJ3hY899zhVS*jEQz5C#2xs|yit)Ez>V!u0##uAYmc)y z0yl7E!n9Iqw1ZLnFTqoZzpWh%NeugCiC-IyvUNC17C#8_f7i0p{%E-#A^-pY07*qo IM6N<$f*~28E&u=k literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-3@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4ee73f503aa11548b1dc44347629007b288ceefb GIT binary patch literal 3456 zcmV-`4S({9P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY4HZd5K~#8N?Va~; z6iF7xi)dp!Wcx7DM3ZxdB?%$Wl0*>MX9=X!-4FZ2{S&AA>7=_KPAi=RheVPP2*DwA zM3EN>v}j4Y#KQuE0h?ejnS0-wes*MK{SoJj7b7n6V_-tQ@!OQ|oV1j=GaJ*65-w<+DH^n_B{ z1Z@z|u)K=SojaF*{rXjwasK7Y7nw8q-<@)X&v!dM%J0d~&wostwUi(x(ltsylaRY9 zkBnAEF1mN`KAEz4^u?vv=ZlM@c;Ms5kAO$rA_~ z8dbmvl+7doOZXz;?B|krDjz<4=tzl$ETwvv zQ2IndK1?3r548`(0P;h}jvdFTm7@Qe=+vo`=+L2qQ4)?8Ei41SEnbg%|Ngyr_3D*) z`0$~tc)YKQPW8P(n`bE<4bnea1WY6n{wpy__qpmph&3_ju0Vy?ZB~KYuPotf{Gy+f`Ln;^xhp z(kgG?zMWD$HK!Q}MEozM-Cjj>>;w5jNl;N%Rwf1x9O$`HnGz4@(da6>n4Ud*%45Z!~^utD5m4P(q>!I$9DLrR`9`AIX9j#lpZVV6P;6?swrCpq$XgoK#ah!K*p_7dd>tr-swC)v})B#_LW__bP zz`S|$rjSI$K`&D^WmgNNc~V0Y25yzob0+BVPUi&yBBDPmDJhYI4EjsVb1`ZIwcenv zuC9p0PT7=`SEZnTh>BaK^qdKLywiDMK$AvOo;`cEShZ@Em^yW;G!VVsO8o~@f^pem z5fIpk=vFB`XM!H@bY4`z8qu#$n>I}h9t@4F^+rzf%igp(mFCH-Wub;eKs)jDO2l0} z2A?}}FBr33|NKdFi5Cw{BwcidSH)vSjT!}ifEYV=tWjb%Cc_Gc z(=qVeH{d9sDxO|RJ!g17ZsGyV4~GpKCi?b;o6xZ(_MwyC>l6hc86L1UW1aZ|BaP;^@(%MoA+i z;AP7GP6GZL`?mjn{UY^BFiywmITN*b$S+>JC{CU{DGnb#Z1O&bpP#3y z*(hpKrgdS?vr#+-?tCmR!PkfW5Gf4QjfwmB@5{9n_zqPPi3B_kKT)}C->L;xQ~Dn^ z^oz`9@c{NOU%o8PojWHjlHK<~f!hoILwKUxF!jtvKbWntG4RcGtF5gqO714n#ooAa zqgc0YooUt(gk#5!i7i{Uh$Ba^P}hjw2U>oDaSccuOVS}6}2F0s@-X>vL0$nuL zYBB#hLgG}~^^w=FUzdyOj~+deW$7kX0s=$CUno7LyQB}Qc%0YQ*JEW(?x&mB31~E9 zj1WanpFVAiGF?P07-1ba6U0V93~rSEK%0M}w1C8ed&wdmM&CYt`k=3p`{^dsAd;aG z8>25K_gEH$JE;eWC#N#Hc*y6@oh#O^T`LwZUM$OklJ-%U#LLv25%a&!G&$~$9Xrv~ zeAA{)avlgLLm2k$+b6v;r%s(RN|Hn(pka9xbo*3}eYZ+6yzl1@1GpW}`OKL!#j<6~ z#KMIOr3Wb}sXQ`KO=Hk(rR&zMQVj2>OFXP;%$PAl=3&GG2>U?1p)Q^sK?OvgmvE~T z!~5y9yg75`$bJu=l(5{pchB=YGW00yR4In{(;*&Q?<-cUkj*wK@z8wl-n}bmpfXA< z;5igL%p(cf2TJEFO;}CNSGbYTeBjMsyn3kpc(UQn$M+zR5`zWSbHg@U=GwJuV(Zqe zvJbR_`apY^z8_QpRf~W}c*>M1mPHvB$7yXuM=$4n&f(6-S`D0@Q3=Pmc%uq#At@`u|&U zGf5}`b4fgmyl2jw5qIv~F-puB&G$CQ>7GA-UR=6#DV6nMapc$J zW+RaZs9ItiMMTp*d-km5woSv*d_iP=AkW+42CNSPV(#YzHQ!3guIbGeMA8SU;(3iU z$QE$hhJFvLdMQAZ{>|j3nb5V>?-=5SoA;Wm4}ZjMiTU9>gHQs-T|5BqwR-2IQ&m-k z=SARugfG%UWPM;SNUff1uSRY(qh>@l{EThznuNY~+ z#N7e`yg2YB!a0!w|1r{TvB z!SYnxtdMYyk@f?9iF9Yx*48$B6U4-$0^A|1ux08N%9HLOmJ6RCe1RB6Q%yts0*Au( z?c4pwNc%w-kS!6Ffmb@p3OS*ArC~WpQGBllpP+q=v>#LfSv)Um7%KXHP|)1)gc0JT zVf|68i-eAm_6m#8RN?{L!|vI$M;tnINV=q8L1FO|BOO#gtV+GcMpKHXf)EV2q~KWz z3%VkN_+mhDBdCBfo;q8^W8jj4fMKl=eQBT=X+IeKU?$_KWsEdUXiYKFI_2f%qN1WA zbZksULVQWYOvY1ljPyhjvGI4PH0I5lCszg+Em~xh1S6MYq-RrFMri~IS%`8K^ji=Q zmKbJQle00>l>SPaCn=eqfH9!I%fKM@cVW^=tOQiR(?TDTn^xdqhNn3b#7;nJqVUOm zM+tqeEd>Py4L_`tZek-KgroEq+WaG>@2LN?eIO2ZCb+%BTrLw>1`)c*Ydg&kyL!eQZ#LM*D&I>X|nuL=v(y4&;;SsMjYu1EzEclL*HY~5A zrlzJm+wL&DvU250IR^}DeW^S$sirY9?m4dF;UPnKWZ>2cOVAi;8TX8hIEu#s=27?r zL&ZqPMLggsBfn;yRzgI_npI!Bl_jHpQ0S_l(3Mp_k6wLmlz+?dawKVO#RG9K1LP?$PKS{09p zTgX~aB8!n$#cM7^79(vZUYsKiynv>gmKf>gVu@*qk!~*HanrqZ>y}Ysg_^Iw80qFD z-hl%L#DxnN+-}=oc|RvN&3Nhqa}gHr#EBEK|8s(@4=-#vLIT^qtR-b1n5(Gb;rF(j zFh*X)!e9-?mS!a&MqBiEyLayv$B!RRDV_m-{vEFha(p>Uvk?$hhxi%Xra`<^%@&aL z$yWq9HC-m>1yrpLUSPx#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY3?fNHK~#8N?VZ_g z6w4LH3yi^--GH&##hMEbY+(^xp(KEWkPwKMa1qLdw?raH_aDha!aGUckbpN{Adm+F zBJ7J05+HUkz87p3+kn~b`D*$!)n{sEs;bB18M}W{sixaA)m7irRDE@NvC7C8Pz;H3 zZ zW1B_z{Q0w*FkynCv0HEZFl5NLYP@;#Ms;>}s`u|C;gK4$39XuiAVp=-<$lheK7Fc=9zCi~oH(Igym+DaaruKv z=*ZgrL?Um+w%Oxm%$Si7+E-+7`SN9T=+Gf``t)hNZI)kGS65Kzs9sgQc>2~#2dF`R zr0lOG^g2@nUSAj zyILb#qt%|e7`K8dh>J2?pk_~UOdV7bI$N~nM4NOizi>9$59%I z7iQ()ogp&!=n#>6dU|w{{yEA!DLuf8LRiGm@OHUGmhX2Z+42(5n5eB?LqmhMcFfF~ zGu4hAJM=^t+LNC=d7_RSIiijqKd#!_+x0$fRDFGYv59a$eL;yiW74Eadhs-V{P?`% z7Q*(vSFc`aKmYI{=D%;{Q>XH_nr^iqqKW4x5?Nb22H*Vb*|W9KTefV``=q$C5xi}F zDHktZ)Yk6QsZ)BN*>hZkHs3#g{(QA&%^J0A*)qK?kH^S*F}(lEl`HDZnKRmUa{0(x zT3Wn?u8Z=LP~od7`yGv!Gjn^s{k+g~7A;z&8_1QEIm6qs=ij_}Q#b6{vu6pB0a?2? z%KlG1{aS8M4yw~q%2xOG_O|BshA7V zw{PE8_wL=(<#N4fRcx(KF_K?#aWMGn@fnz7*oYA$)bQcMgGEM;DROk2ca(C2 zcM0%$dE8LUa%8=H@ew*kP86_q7!!dho(XT;L zBhF!3JK0DnrhK1#Kj7oguceT+OThIpzMRz9>J=)wkI;G64!N{;vU0p#Ok|9OU0q#z z_yFR|bhdV;$hKcdT0%tzKDC)mX@V&-z}QTagI=_Rhzt;aZfk4Pv1s{=Auv9_&b=&* z<#S~vZMjZDN7im0*{nop6uA_Q;cb`U;`#IEIz$S`0nGAUsCJQoYmdgdUXKkn5z>eZ zurNSx2Db8D2>o0!$~pYFmZYtnYz%U?c8ssc*v8oW?%lgQv7)@BwUeI%@;LpC_g5wx z)lX5oJS?9}jjV4Pyo8pGfp?bHE>vWEPL|);*yv^LIIl)rh0e2fn1I;YO`JGU?+Xzb zL&n>;Z*>lYXeF&3V~Q+~R}Hudov?O1%VT_owHrBdWN=UJ=;+X{1CyZ;88}%xQ{-x; zq|j{bNJwq%SY(ql!v#cs{P?j>AgyTCD!NOLj;)tyX17TNfcstPZmxe>@=?OIz~^LjMnA~H_Kno&D0 zKN8K^+Sx=d_GR`5E<$S$ZHf#U8X9!Ou2|Hrn8*MVAp%LSU%!?j11D?8$of?qE<$S$ zjTg4Ygo+Fh8pYY~2M=-`H}vQ4Qm_7|sL<@8sR1pv#;}NtkI2BZc4^00Tm7nuQaxIG zXxRu`V|+yhy}iA9e3l|dxwo~m)vH=4C3Jk|TQ&w`G<-A&5m{mp%gNd?md{rerG(bV zMj*!({)pc7g~;U3y{w)5Jdnd$f8$&mvoS3#E!soF-hmL2F=XJRZ0NOi5>w=Aq?FKB zYlmR})TvXohYsz@Hfv{y6=dy9e11`tpp?*g)(&uZw6U>KZ-)#Q5E&7>yLa#EZJSsj zl&hR_Q&4EOcAq|dimjbVXisKvi2-^u80aE{Vm&%x?HFU_7A!JgYbVQ(wwIRD!3a+9@oC6om02Vnqj<>a|1&Rr+t(_?{KyQY~93Bk;=LsJ@dZg~$ zxg$jeUe>NwC?>QuYKQR|v1o)xLqU&5Z%DRPv1IgVPvdb1FX!M~;Kds?itN|EQxnS&m$Z>9+atHB#LZs>r&S$=bK zGlJ8s3uCDQv$KYlm+>wR4rs4km`|9e|tMSc33=N&IOP{C? zO|njuenY9by}dm#p%-->Qe@=jMU5e#44fx4Zw%s`K-~2^NJOn(4quLz7!dCe9 zChixv39WBrC!zHd0|581_3PKGUAuOvjT)-lHrd{=!CW7WiqTCL{4Mv1~WFHV{2!|B4Ft-+0@tPtq(FBNC?f=j+{V? z4ESWEL-uGLwUZ)eVy8z6UGQ2vSf_HVwgxM2Bye(pc5Xs#@E_q z_#=h3M(xm>VeN2(P(~P1Xlv9C)(*j-x;p2lw`B-oq1oEe+P+Vsc7q+WM<-6mGMVV* z!4x|0T02wZa$aLM7>U*6s9k4gXI|89>C&ZY-MV#o&-#?&mm}1fhMEYInnIf*14QlM zq03eIzM>`q|15MzM~Cjwg_hwmXXJK&vTxr${q$e?uS$HugK6N+yYPbPGQ$b_yCna~v?6S_iVLRW}P=n9buT_G}|E5v|Y8LnEjN}my0xpHN+Ck)rK zCoT*~p>ZuYu0g@2#%XXpd*Z@?6uPg;VjWdXtqZNs3guo_l*HK+7iwMT_^eQcNt``# zp{B=Z5RKCV>p$IBDPx#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY4b(|QK~#8N?VZZK_pp^1(}2`CcbAyIN)u5|N~KOxFXqTEPPqC7y5s1G1Y z0txK~(Lxhdh)d|jV8AqES`7Jp=j`uzcFxYu?)seLQ}{@uIWykfv-3T_nfXoGwfM|u zGKv;X?91rUqkl+I4+=#xBfoe{q2~Pg^M_(dZ>&O+h(D$1M-(b36eBOsctznhg@YvK z-(-#lzCt>dxTd6}WcA07A7hygpFVvG6f=Cj?f7(k55sBOd7V3VUQf|FlV!GaViTy& z75Ofr18OAn@Wdu0+C*1%Hhr1l%X39U03D2dgiS~nrY|#nd9H}4r@gSXkzY~t%i`kV zD)mbm8X82WPMt)@jvZxQSX#6Q!TQnh^9{Xz&aUs&GMfUF9TP$9@Sj?R}SLQK#;rJ%?e2$r@ z$D7V;3VZhK5gRsa5Qh&RmT9j3HX+?Vha2=zUGPB_kqKKkJ!YaFZ#plT=xwMUenO&$ zYO9NgxOMB+GBCEn=`j=ac++{wB#el_PsljBnCN84L6x6vh0|ju>hY%YQh{D>KdeIP z7i-t9o#@%KrzkBgwH8i~nW)E`&PxShLS~bVfCmMMXt&Xb4mh^}^~g!|80YL?(=g zz*9&fKB@s;&Z%B#y|5-RY{#37erK(kaARm54wnL+O&}Y=!IuYV%Uy1o!6WM zwM>(c5?w^Z7}a13%_d4di3og!1geN7B_(p^2M#$fiIQ(30?#|9&IT)oUlkS>jzyX; z3K17AS|sMrpD&$q6_Xfx4BotXBWi1FMRj$xc>eskc>n&r^Or_+F->XyK1M%|F`cjH zV?Ofe(WBzz$&*e_jPmmG@bi(Xkg7#iR#u9oOP5L^RrEsXH>ut%M7(k1hB$rtw77cp zs(A9`iFo(!o%2_J^g^2mdYl)SA;&%89#x_WDVG>pwQ5y`G>Zu#mn~Z+=FOWY(|~U} zXm!4S|GwD2f4>ap&YhEa>W`(y45z)?Y&B+r9&b9&3!9McdeKPS88>|?2rDAesLrs& zY!WdD2|}_nPS#jv`ce=@M1VFCRR`$T*T09;86qNfTwwc(CtemQopJq3`Q|`W5fP(f zL7S*wUoVXQy~#S>bY1|kZKWpmF%fr(3GQeL3JT&K4d2|TQKMuVIdkUBNFsvWyLXE% zTegTZXU-%Pu6_IVPbtF9zL6Q3LA1R;EF=jfT_4pFr%jtCR<2wr#*ZH_(_uw~_&ay* zltQA{2AYV+DD0>30(luiwB2Z`kgAAXyLJ_`X3dfo34bz*h(HxmtyU!D6^i~$H~dX3 zC%f=!BPFVcKwV;pDx!LvDxw3bb0%40DBKDVY>^aX)dnc^(4j-c^5x6L!i93bKx7eN zk()Pf7N<^~l4*eJ(@5zSeMGfBA~T~As2ZjMw+e(VBwG%IT(M$>Sg>G0WD)D@>&5o% z+r_qR+r)(n7i1n^pMA2)fSDi6_rZt6 zMow6$3BT(w7;0kLi#smHPed=u%gZlQ^e+lqsQvtp%nXT9NL55IW5$g5s3wYt00qZx zSh5E3?Afz~30>3qdOoLLy?P~okh^#9%Im#-`!+5v2_laP3W<50B6LFg>D(((t<1A;VhN30zDLm)Bw)E}k=T7UapFWVcI;SDSXh`bA_D9|>d~WzsHmtA0|pEbRaI4D z$dDoNQCQ!;eMR@~-Q_O2ni}Kejij!wt^R*yWLvt%bMiFq$NuV%r;t>&aiO?H zLP+cxA3l6|Mu}*msHjK^h#CEX0|&~v0puaT^|C=?_8>9wq`^xP@@OpAAES^gB0$w1 zF=B-Lr9mc(r~=_&t%C;-mcnAe4s!;mRq$#3aKvmun!KZ9QQdvfg@j8mncB$iv78tX za^%R7@`kcSL>=rfM*EJk6ciN5?TI$)Bq7^T^gq(2##Y63A>o`(Ad8HzwbEzV-atr< zw?Vdv$Z(bK)vH$^>qMJL$6!}pA!*>-UdDx_9vh>Y`5!)f7#GWFbb?t(RPAgPQAM{- zloLL#7bIpE64TmV#)TxY;MxCxF4`Aq7Lj{xw2^F)xqxb&FJHct>+L?4X>BiK4PJ~u z8>e!O{{8#QC6qyf201xK22((1?1?I*L>Cb;Hcs*8`&mJh1DYS4YZfANX#o3D5Ve^ z;we}yGT%UZ2{HTi!-`8HRK3wzRO;0`?p@O#J2jWJ`!$TVQ5oQI2y z$ywL)Ij!dh9e%({QvfkpN5FV&a{dc5hp zFrY2NTqCL>rug8xK-6S#`SNA4ZrwU*nL1uVszXFMg+^m-ITs1IaG59~z>p4GlramA zWd;OHn+2^seqeS8Gwc{X>O)8JVxCiUiSpw!=&FzyNRJ{RWh={t%Ty6{FeQe!zhE6G z8DgUOzV1_Y z;{3=Mo=u^EJw3*;-0Ib<#hNv1WN#joFg>^D$R<;vn?6M0FZA(G6n0W!u&D0|HVhy% z1^A}$RpDmY$SNTj5)*C*+NPr<=3|+u!MiS+0-6N~iDxivWTlY65%Ua%*h4!h!qju? zWUY|t0uFf)%g$OMsolb29acP3HscIm6#IyJ1wz=c>#&&J!wdUnXxhvrsvNFhg^5YOKl^ZWI5!KM>Zs4 zCJqrp4G%$@hKLyM{GNoAPe{atbS!aAZEfvpq?>yJA}Uy$Ub19~d^o?U6TNsQXye9> z@wb~YBqBDXZXywX70YqAk*LnF#H<#P*QT2)qNT@1!-)r?;;2HRO)Snv5z!{1I>QpN zHl2)Fa!Ze$@Z6+lubmAdQd1Ks*jIWy%z}ha5Ebj@?u*#RAMj;uYF>_|yx%#|8;P z*3{IvEE3Pxp!&?7JzJ)89atkCgWj=Yhv%auUOhGl)JEnLmo8nB)!FCy94sl~DO(o_ zLVAg)@2JRCV2uzG%dkG&q3~VoVhM6`4)EaKlO1bWX!P7A3Ei19NfB1|q06IMi!Q$j*G zY zw^2m|2~RcD)zuldf#d=Zu(-Gwqm+M8*iGX7!Sq&uFsM~qZV;fVEjI|{5+oj$2QagZ zNm+Px#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY67NYwK~#8N?Va0` zRK*>~8xeFDWaYjKa#vO^LgXSKAVGv!3Y1n^QYjwtVk+U$yyY*T@{&}1h)KnVT#8Cj zl>(Ir1#(wJ1Q9RDMNkB}BR7Es1kCT#(>tFx6 z`>eXFyK0x}VCtG`?AWn0rRpQ)e!Zdgx+>+=r=Nbhq3|*v)fBEkog~%IrA(00B&AMD zYv>0l-%438fd8qt8GElBI{eXZt2=8X@6T=+0RGqS6^TM zFRA{Kmj&Nr>Rc57`+QpgYwAH#a8J1?ceoDN;cQDd;aY)34@k+)@8=4)jf2fyx%u2m zz;^4_O*J$$sQd1_PrnXRhYpqfR3QKOe#Vk>g%t+*5%BZ zGr9!D@w;~I+P>gc0=9qu{%XpUDQecNS!(Fep{lN~t_a8ibivu$%H58I9yoA7z5DLF z>cbB|RG)qJS@C#Q_I2KHz53j?1or*+-;V`rd~=HwIuyCc8{F0e+tSjajvhU#HgDdn z-hA^-wQ18P_2rjeCV@rArJO4K>0drAm!20Lgqnjrc<`WFyLPR5>#esm;C=h{C4!Yk zbhlJH^6Ee!FL1?bvDn!HHnxM17g*fD%9ShC`t|G8kt0Xaf?XqJqu!8{N`HA3GVb6N zDRTvGV{3rLzv2c^99V&RLjDBoyHYla)o$axyj1$@D^$SG&KI!5+Bn#Q@+U4}tw3F> z-^f{6-GcSAkzk2q+W=Us^=&C{)Cw#rJ&Out{OqpnP6K!y26oV( zL2AN;32MZM5v>7szf?7M1FkBDYZ|df2o=ure!L$L2i zS=t)b8dNowOCvT_tTr_4>fXJ(hH4wI`uckPUNvCR*|TTW#~**J^91b)EUE~uaR;*n z?1(V12zOxpY*w)33U=?_tv>qbBb_He?FB5V)N*4RvEIFVtC1r|YCr1(EJ+x^e)ZK? zdRuz}i%QQhM#gFv-Fxr7GkEQSC7z@Kn>TNsnl^2kZp5;J#m|z2iOUrT*mF{0)?1|< zk`k8Xs>x8gtJ1TKk-%Ln)k1+gAPnr_!GqPa&pxYtEj_Yo!SXrs10-U|7bN6kNfum^ za@y24rG$yp;#nyNr0kQjzZRfN&on{;mPs713fOsJV0-rLsXGf;Exxu(moECfYQSE( zaz(xO-g`QANFpW*EXPIe?}^*LSFc`lDo#o{EG2i)9Zemp#>U29NcD_BXymnfg80~{ zpMIK)kQppt7gqbpC!eSb7cS`ctlZCJglTDS|Pnk)dix<9yY-@fX}C!f@oE0&?RRRb2CIB`P9vXuS%X`^n-J+2GX z)>cXTU%GUut3Ym)x>?){^A{eL^KAABIT5aD$mVzK*m1P`gHdyHvxfWVqmL>H(aON3 z1&h@Z&(eT>@WBUqn^h7hTU({w-_`=SLCRnQd9VO(l5#^JAJsclsnRnKk!{EXg6`@D z!QyLY&z`Mbdg&$g&_fTEflCXP*No1tUcFkEty^jM3nFf2*!}+dljm;mJwzJk&!1O& z_wLo(lE7L8UpF;1VYwvJGlh1gXYa^1WCCFxuY+LG@ZrPN3opE&ql745Yvpbaf<=c9 zAJ)Eh@nWu}QlmzV(r$*hneMb$mELYYXXPG;ISP!YefQmW>fE_=+HV7!-d!57RsuM4 z{17(IHlhTOSxXR~L#_+5M#^A@PNBaR0>;bJYC#^VNU>12Ta{_+0V`fJG#Ha656caMFtJ1?)H9e4`6i%w!RP zW%h_ywQeG^K{GDlc`4)d1|xf=X~>8Ws4akM!6F*NZr!@6K7IP=!t5J~tIfOFL~oJr zPL7YSFsM8~%LsW*r$SZ>ovlP0OB zo_b2>6HpegwG^OfBhA*-CX%X{Tkg#V6B)E zK*FD}ElScc0m4)%xX)0P!4YgSdn7SUMKwlLT#@XXdwVcvS$tgA4%4N~gDEEl-=q&pp~RT}po z>MKRh;c!*fpxMSuV3ssy_*p`*$&)8*_fQM49GBz^gO%h17^q~Vf&O5;zkZ7aBb(|=FOYqgJ2P9KYC>Vwjo#^VCC3Fpxd@>)8lCr zhS(YUA)`gHusPw=r%&tfiQknb?^}uZJ~|jg4J4ONohqN@FL2>_TJ~92*iujlT%oT! z54UdJ8pvx476F%;G&p+X8!S+Pw0-+_J@wv>NbK6ROAk~6n@kp2Eoz2=fK14QC9{pp z7s+z0%_n4BA#2zU9Xh0s<4UZzK>(iuw1|b z52$O_tkD*`ZrwVag9SKp?<6b6-2hJd@Z!ab+FfuPV{Jh3CGHF^VpN(^;IhzdchEJs zaG6mvNlupK`bCi_XGDN5FWp4s8|v&{o|gXPcdM~yaN1T6i{kzF)753EPVcpwA6YLw5fk5 z+g_LQKLN}zc%%rQfa}A0aW83IOcAclhDjJxmJLI`8Zo}}^UpumA&nj4c8^CbP!ku7 z$TsE^ydmXn0bDx&DWIJ@ck1K$uwL39Tth;>K`Usvk=Zbx!q;N8#IPvrcm`~$1tSJJ z`@9op!E^w8SZ@lyTT~EkWHyZ6j*s~Rh!~OE6swH@iw|X{OX@iS0l*ugd@$B)a}rra z)R2aU>vKa-m@i|Jh7aLq31`BNW5k?UrVyn9Yi0IdD}~R8%1#WamCE3v;0ZIvAM8M6 z5iWt-(vs$aQGM}fT5$bz6 zKBX3lwA?&ISCotu3ONpETuj$x6n-~!*p`Hj{SA35)N-u=;48ij$^~m)v*M<75k&Pm zCZ+UQSFNB(xIqBE0Ec}~hz0u)%cib~Yq(N)**c2#xi-g;mmILf*vXN3Ix%^$UK-HC zGR{^~q|dcr@liaok4$`4(KywD&pmYq{mPxBA7njUdEfj>y%I+2y zEH!ZU1qt!Wp)cA2w8>I zFg(wAT$l_Pa?13*IIfJl(A;F1JNk_(s1K{vE03mUMb3w_91FDD!) zW5IK!EE155rTj|DOjEOtbrJ8I{UXImPv+Wk%&fBc=rnTS>PD^b=WVcrXEqn>BWggL zC@0K`$4S67N?|9KZv9rutEOJacHWo2BgKk(rhzioHmlX*637SJDOMpWdDyWFx2#dK zaW89fxgC7D22PX{=EMPzB!M_U3hUx#QS-X+PPF*mnKNhVCz$wX$Wpij*cGQimdv=5k4whyN-P(N<+8qY zSuOA|S%hsVWZw$b+^ggG{1DLKUs~8z+rttk~AWP(+*T?Y?BUaz=_${Ac zv0A_~Z#eAtgt-J1ngy31s~=)<>eMNnVQ~TTbdZG$7pm#gr|Z|g#mO*%gTh<`#=J;< zy28TkMd4xtlukm{sK6DI`IanMqLwXNX10_{;L^gqo)#|M&sPO**>l7YOH;9u1q&8< zwGdZ8riBJ>Sa*}uA#n2mYDHYTbm>xUxlvJqz+EBLpXB|L=aWT_-dGEm-OC|1hwKgZZ4A z*%rmQagI@7jUwk-lr?BpVzIlWVbc#ZW3>v~T+lq5fF<unP2?Js~shYxrrf$y~ktttF0i&a>%MU)UFv07%kFLNV=pkm=7fqO$L9^TMT z02iK|=7?_VodOt3r4hp=L`lFB*D~XMnHXT?#Ir=Wd{_W8{yzZsP}o|o_6`_}1uPFl zW*KbQGE^%uG^VTmSFCn*VN19Y;UeoE}<>{&jhr zJFrRx7a8|ZCxCkj;K=!;0Ot8Hb{NF>I$YnfjTb%^R}g2h0+wX~E2WS_NSue78m@5< z%o^^T1eivSCG_Ne=~?++hwDSu4FHs=Ag=L3#?SJ|!GDX@rkeGh8ZN2?u}YII z@>xB_$WG9s@S4W2QrH$I=}CrSxI-v3s=O9JX3t~^STFxEB{N(r0fhxK32v$Y=6PEc z3DB$@q5#*z#&smgx*G~`2?HsW0JRdZ#Ic#5SB|{FMUE8@7s!dj-%+x#)6AtM+^ZI* zl|UuKg~fXNuYX?QS{W;5p6wJVL$g56TLvmQSswAfOQ2?2suff&aIM-hA%g#+i%A+-Yuo*QXn89D+eep;Ne(rKc~^IJCrGLP Y0}Si?p@f|xQvd(}07*qoM6N<$fPx#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY30z4;K~#8N?VZg} z6v-RMOG$*)3<$`Z3Mj6u?-~>@Ui@vc(Sw*66Zf|4)ypQk|HvkL+3XF}gmCeKN5kIK zgCU}#?&c>7M0pWW*qB{o)U9t#KfN{e!VKNj-37xZX{)R3?wR`XeCMgI?&)Fctm6e6 zf->aj(WC$4MI&FTM8i5V|L|pQbaeFhd^Jyjh+M#4UVP1$cE04$woUzU|Y9tW!2TyqV9qX8?-@zky5uKHf}V=ghnaH zU0hsbuV24rFJ8P5by@I+hK3Qo`U@o&qlmKNTefUr$B!Rp`}gk;TJq2vQQwf-5!-gQ zj>J7Kp|;!Vw&RYBjIi6cZ?pUN?`siTRK#2^CnAw(X=!2S&!1;!&YTHfKrIQio!(%n z`>k|)+kSER`0*pVe*HQd92{hK?%WY=veg?K8w*4%{4*%fjzE3m>Kj(uSF3&zYHEr-e*9R|At)#L@+VdZO~i5*5IlSK%xdN4 z=4Q>xKob#|gd~b}Of8A1+uQa_0}{(5wtJ z5rOFd#X6>zMAYqV`y~ZV@gU&D#DuZ`HY)>7MAHG(j;u8S0X_%E$H&>dd-p_L?tg&M z%0Lql7!F|Vm|7B1x3}$=G?Y+)+W(+#v@*~{GzmzoRRI?k7Q(TiNx+ttmMOkEX0kHS zM3j+dbJnVW0O!d#&BKh1j3nqdU-CXTU=>joQ0=H%5>#JJ0s=bClOX|l6e6LF@F*M< z4FRfFu;a~hqc!YtUSO4 z{3+jJMhgM8BWme`fZ*M`ccP2~tc;udSK4#)TGd4WZf))+y*ic3m5og^KwvPECAW9^=UAuOPG7gABx|*jFSlM`*r&5ps zg3raQ@5QO$O9Nl(mzI_)(Vz-_;lc&h+uO^I9XlrKQF!y_jYuZQ%BDyxB`XtJ8s$P} zeODOL?Kgbs<|0TL;E&ow!RwhatpruPEU)oFp?%lgrMB<@C zhhhncOQD#tdG+d5(4tXNRwjHD@hiSGVMP@ZiDdWg-LVr2l8K9~On4MIn%(P(7=E4Q_^u^l^h1TE2UZDrt_h?=0rpBKH&k3(8Tb^51y&|d@9gaK*2=&q5ko5zB&g3;2409rR_3Q-;VB3S zh;!$iJ9i2Zz10N!>AS=rP;yl@RD+3=y46Q5+2pSt3MM9~os}uF8Bxz-k zsfgUl-}0hsVPTcD-$Fj7dVy4S8nBHztj_zG(`+2Ux93CmsTz^HeB{g zJyA(TL>_5k@|CQtekP_kW3%j+dZLn6B#Oybtf&H}Wo3@@ghC`PVvtY{@+fRo1q3Nt z*?B@C(uhQnP)q`X)in8PBMlJ?5{gV$SC^Q4UCmYoiHb;L!|Cbig0UfiI~#C8YBgIK zBpr#`*pNU%p_|y_lBJbF(jr=|>?)yTZDo*{h{dezDxqX;WsqbfE^B2MV?&U&l|hms zmW7o;A|jTPl|fRGSTqm-MKfy+n?ucN4}Wh( zD-o>;#np8b;G>m6Y!Qo%4P9MF@zu(}MI7a}G;HUz#}8N?E?n3Y{6 z6rZgO;zlB#ZfE?igv+s^&sGL;Ma1I)zsRo^Qd3hSL|p4u2C<&h!=p_+%$M7_;1|}w z$&)A9sZ*!KQ;6fRw6w%--MYo@-n}b+okE3+i0~}wVUbCZn21Oy_(}yLG7j(GzZVbA zH3Px#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY72`=nK~#8N?VVeY z71esj+Z&34h{$DBt|EvaB8aGn1BzheVAav0DC?vuc{q76Z^=t4PkB6*my^nQ$fa^# zPNgi0lw~S0fJnSw5m8VPK|~Z098^Gr0Rhebx2C^+)?T~2ci;AmWBgaGTDyDi>D~S7 zufO%JZ>_Z_`dL3~Q0lz69&p`!^UX_CjZwM8?I@>HDm~k`Z-1_Db6_1Ss7hJ&s(h`oM`fSNF_i%W-mh>4b+xKFDl=6KBxU497hN*s*RX2cTfkH*V`n8F_ zzxd*d0se)`rz$&Cwh8iD)g4pE;09JYL1nC`_TRCN6K@#u>$z9{`Y=(5W$+o|dmmH(~Nife%zSnEuI zZEBoMSr>f86<0WzmtTIlE7x9oZ8Uc5*l5_WVeT^q)M|9%#EIz8p+nJu0|%mG$BsGJ zr%#`bzWL^x==0A%Py8OgHnFYm!6)O3YqR3YaAUAo>@q!OGt(|03G9dwBcdy>ywX9u z=9+6w{@PjYkt0W<@4ovk`u5vzqk{(zMjw3e zfrI|>#~-`Trk$5<+xh9n0{Cf_wN(LEg6n}@A+XcZF2DziC0=#aRneF+W1Mx4A3xre zapT50s5*}OtN}~|YqiO?vEHw~`YL+={r98YyLU%>_wIGD+4t$E1PXst2-dcB)A7UW zR4M^(KEnbk0~YJN;f5QcS+iz2umHuk+M{nO3)cTTtQw%WjZZ%L#1*W0-@bkBbsAW! z<)$hFz96m(sVGdGh4wy6disMvop%2gALOD@g-OttViVT+%3Q4v`TY)#nemHw&mR+#+CUpn$qy z!Gh@a+i#C}y^7Hhux>Z_d* z?JW(s5L{U;7Q0Gd7i59Om)>#59ntdT%cGlay2)J#k7+wlodjzFFi#X#%u{sk+&LG% z(dhZI>*9LkzenksFW2Tbf=fJ{ONHRZR=Y$eNG2;4ENxH{952f2l`gHrs<4tjPcqPa9yfU>~g2 zx}J;l^044O<94=DZn&}4cB^q1kpvchiYpi_U`3kp`EK&z$gc`$hV$qw&dUL_` ztu_fPK8(f-A3K=9nm8WeA_j8i%ozum?lTGj3oguZS#K`5X0_&PP4?&zZV?sGcnvnN zCXS2$1u(e@5<-U$A9nBgfMe@DA|{6fn9+_@^_^*kWVIx_XIg^WWMH}DH{N)o3$+F- zSQE#^PzW8TPG!`o)pBEzpJo6cQ*Gf&9E5{4pZ0g1)a==_ zF)CM302Yl^VPqv!ZQX**|5vJRmrHQnDjzJ55z)b&ci!n9r*@!ayLRo0 zcI?>U5;44{kdPtUMJ9~%AciJ>txJk&T{`o#&pvbc2w!}kT*EN6GheoI4ViFbm#{)E zVIJE)SVVsvuslk2^t^naTeohFUVZgdSKfN-t!Vr9?G7^Kz4zX0KqfXN%?OY>A=6zZ zEF(`WfNq~}!N66uBP(jigsVG49$~RgctQx4FP}euestGecex%IstT6$;L9(+?8rEj{q9%R#;fEi(+yH<{H7c5MVHM$3RdFnCH0Q)q;)6vo+^vGU zy>Cye+QGe5)m?(q#kPF`HNKYci}a!2wKbIiyJ^#==!F+vaB#5}T5aTiuP?TNyJycH z_ZSmG;rr_XOx(+P@zjJ8TzVUuv*7k!c@BjaNV1zGHxWWzU|)IV75B)a@cXPb*|t4V zZ@&3v^x}&zMo&HUl!HyYT2&Yj*Juq}QRRXgULctzOW)NA)*6rS`>ZzEcIc|MZQJH7 zAHW}d^pUf?Dk97TF4tQ|r7V{onT>JzDgqYqNMmuEH*c;C*6O$4e%m#IZ@lq_8;Pi@ zq=bH6m9kvE48XL|rlMeJfOhWO8Ex6JrL8NF>p85lQ{^r7j&`cJu2b6Q0ZW6(ac$o~ zsFdY~Dhifutcp;I@hlV`-)aHLk8nr-&SqqHsO#WwLU4O^H5?@txBT=R9PELduK zSA<$2P^$%&sYXoDXc>Ktx0xBUPGOg`V3U8IbO7q`yJc0#a(%F-GGGxCIEYJ80$AqM zGEwfO5UeTQ|D?*>YSo#C+j5>9-@aoYl-qLa1k2yi@t|Q0!6J1Cm=sMDoqLvf2-ZK| zKq(j85G>X}kBEsEWxyiBIKmthf{3>#t1MRlPolm>+E3SfrOGV|>0BThg5~Bhgg#Kp zZMh~+oJ2AWl8yNn1sp$lS zv3OC@E|hgGh2YwWlc6VhLjS)ESVW4F1UoZgOm=(&r3PuN%DsB|XDWXwsDG#OTfKf% z;EPaJVaY4K4}u!s%_Ngoo^B-=x<{`cu3nd|!} zDu1r>h`zU5VBIv;q|YP9@)#i@=Ui4~;DD>fGu~57?cmaC_l74PU$Aov_+4Llz?!)5 z>C>mXd1ELI6t#ekR+*$S&i1E$o;2vfg$v!7UKNpQw1LVpY6rJR#W~jS0z$uwihxCC zy`(O0x#bqOFZ>>>O}3o|mXQ(QQdhBBjE7E+4N@kERYDkFsxp(F@~QjO@XQyS$vXLA z{)G@N&>E9^BsE89h4O+maSRgEG#X4T!cszDCfm-k*zWG`=z#|wXbdox6$zzhpM5rZ z{`u$QJ=*?>OOaMbusVhMfu`??c0;AJaBP{Oa__Y zUl5{E!(y>sT4kUV0c&E^0Jt23AtPQhNe<}1wl1s>cE*eu(af1M-54HVfr_845*?VZ zT5`+)PLiyaF85Qi+O2(il9dN8%Sh+w!rV}@fi;nk#`*wEQW?NSz?uL&Y}hcDoTjBG zjN-N3I11Y|fQ;Qy7A;y7End9X!38!qqN>_iEfU-1eA+{9=qSD`qh~;XA&od zFcp90%9YX5rAwm;6Pl(tR1!Xz1`|I_jbFw`6C(q=N`sXq#_t|`?6K&HC!UD+G)qqu z38ly668^StPr7oA;>miiQZ-dD`(h@^e?-ZdVzKmwc$}?qt14U!epuh{tQlWQ%_vA6 zRvL=XYr}>OZp4`mQj%n~+f@CJ-mhmO9tWaf)1y04P)GP+tv2@n2u3M@J(vX8C!c&W zdg-N?oX<^CY_;oUwg1?+CtJDTB0;-W)pS9mZUT$wA~NC%WGoo}IamN2*yo;m&J7Qx zNk+p;OB%9VUAA_e2ww!ur{(ibgF2V1J^x8Wl_wdt=hVU_&}ui z?iW}No(dMxLn0YN$kd5Or*07f5^~XA@Wf?_xw&!;SLkJ~l|7koJ+LbT*3Cyv14}QB z*prxZ&YU@}MdyTn>;kTJeykx*ixiLmkJ9IIEuG4Qt3P&Y&IGn;d4E_Fzyp;ymmU+d zafn9;i|7&3LBSw^O(%%yqiKo(w_Q4w3RhN(#jX-q=U>vm(gmfbL?>e2ym{_K2ZP0i zVmOEn5SQhV+1&J=~OD**lL$3v}&y0NdikI>i+xhZPd1BiEv}9{iUq7v3lqK16WgDz_MOunA2(>OL7m1aAT|O*5lE%QqpRC zEUuuFV96zrtY%6Qlbi|k{X)KhggssDPK@ySfaw^e`CtJaEx<>`>`MV|O60U8Zepu_ zRB>uI+aXw@0+#ka^w2}KgS8}$0mNb$r_&P@f8vhz$~wh)tX;d- zWy;dV($vA=jeidFyGd|e225}pzp7+4B92|PYL!d%)dd!)Owngi3D(NMAjzP#i}9uN zbva;0Lr7CHY(%LOGMTb9Yt}@MKmK^Ur&)qa@_dcX_pkf*^r^`$=tNmy@}R)ZPXbF; z3O`F?8mp;8OqOFPg$c{Azy5l(apOj3EsW3Qksd{NL%0zn@3}FgCdqBob&ia(a7=og zHY?Lt?0Z{Q_cX!Po6A)#m9I_wbtOhqv0Qv^9pM&W*RNmivi`s|pyBuC6+JDi9|$}R z2ryNBGJns_;zlys%|et=f{X9^Sf6Q(PK0pXnlr&|3`UyFYHHurP%CKT`&*V60_^MhkoeS1lB(()euAxCmJL!onR_M_*PQwK=X@hRI6rUiXGR%E+~ZF#iHiUx+Lz?MasQou5F&9Bn^haSP-$`agSIR=5V>=2`fcXu1CAE{_lmLY2WrTmoQi*-~9#eZ^M%Un;4l$&fjZ zwEc!nSrI$u>7BN~^}*I9f_w4=3|^*y#m83uVagcrCLJ$zyoOu_r9YJEbL#?(#d1L@ zV3~183s(uOC#EHCiI3YNl0bAU`a`Kc*9TE%?w2oG*rd!>tP?7GYXa+huqwKs1GjWC zR=-$o8xa?kTNeH!S1!w~DjLPK*i!zOB+*=jHsp~jOUI$7IZuW8Z7t$F`fJoA({fQ& zmw-rmv`L&WJ#}dernKU}=O!)GO-t-lHa|+7q}X~%9IY-3*aN#l z$7^2P7E7P0r9+fgiI!`s3p9Z@qXV{TCrL4o2UUJ82!&ik{CT(NPsd69F*R}m^f>Dr zrN~k$TmwrhuYS9GT}8mfa?-39#ejdI@~A%hn|6?SpU>%~TlAM^u_kUVV{KKHcU5Ie z;zlsL3@jZAk{ygsF&OFpGDuO;kZ_~u9g$;56KO=1N$M3&)aF#3*KC#PDmQSBap_h8 zJeJ-qsI%LpAIU|Nd}88CTH~5Wbe);Q%a|VtAzxwQrUh zrX1-)^@d=n>AsSIw?V%u;sUu)E*NX-r0ND+bXBEqxo%0+$&)9i`(P2i1?y!7N>+=X zSHzhd3oF*$b{4HiJbt$xapNd;fkj1ERW{(VLxAV2(V8s4O-oS`nS6%Ri4p4v!Z`*p zCw`nIPZS0ct3oCkVX@z`Wy_q8tqW|KRh136NN_RB87EGhXqvY`jr9T;OT+)xF>4%) zrLkn#Uf=BivrRn8_wb#0M2N?n3WolfCk#{oS8cp}m04BU)VI}1(o6cQo}QkSq2-z$ ze)!?&kw+eJ7gD#d4nj2&>-e^;xnhW$V^6@3V7?eXX7mqwzw!SqQ|F554z&CSKDMzE zeo=q*>eaP9a!E4Kz)cI-Nx%TN6F^OXWf>J<{Sj+l0!!G_vZ}JlEg(IfWRoT-)NzYT zOa_`qm1GV%S{kJ~{BM%5N{)}4z$%jp@9`Nv+X-=lnFc^i%g}t9>9TGsWU)uKeW`rrWpCXp~cbPv9Mih%L4xB@H|u;dWZB&!AJ zfBNBoSb-(Bo+XDTwoh>ag&q@7Zm}l&vs;HcPHdqD={oJ=ig54{H(9Ga8VJ!$Yyq9 z%~B;w*uEo&+wg9=ch0j4;SM$DraEbbuH&-yAfw5#5kKM2_NAO9eFbe&#p(@p^O zyVa~tCxr^wJc@lRU<>^>>%!ya9^hpHJk#yWDf^eSELb5Q0CfCvC%Ug8P#Rr(iUcSA zRC~oy0r`T$|A)QQq=Xo7sS3WeHmIiB;hF@QQ4PX1#^d=}&;bV>0NtaqOJ%p9HbxKX zRL9`@N&wA|Fxns-^~gG*#}JvDZ@<4qO_#CbKz;QKTwgJ0vTW8>W5x^vqapM%@*y(3 zZNKDB7}3)oKz;Q)+%zRXFQWm0__Kc2Pf1bqf5jNB{IOOm%K!iX07*qoM6N<$f+i2( A#sB~S literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/display-9@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..53b687fdea4d84005a44887e130fff5044936aa1 GIT binary patch literal 5195 zcmV-R6twG!P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGizW@LZzX3P}QzQTY6W~ciK~#8N?VVZB z6xA7q+YQ+S5kY(nO%Lzm*G)=JvUfBS!|u9CJ)?^wCEL-+udT z@WmHj1lzZ7PyC)K@9zv5GUQ!l%axuj+lotp8(QnnlxD~x&hjt7lyt$vhYzmLLflK|r`a1*6k zaa(XbYn?2xO|`wrzhJ-}cieHo=+UDa0Gb927~rgy{p31!@b%YU2RnD}bbxp5+7;~E zw=ev3;J^d+lTSVgwr<@TeD>LA?k4!>1lJ69J@B#?LkQLq3# z@x&8@i4!LVW5=k`t?L&HN}CA=Eoj;EP(%~w6G}PBf>nlS_Ml{2j;~WUmQ%IK0Ubf z(o5a;s8OR_(N6+Z6fECUYzv&Ar>7@4_0&_{Z*e14$-4IdkCMQW>jsV1=SOODz?d;( zcFPrQac{Pumf*Gu7E8VG!V81hvu6jFTylwX1;-zMe7)jI0@V&!WZT?4Dof7ew9`&= zkSRgte(>9IU`+x%vK_#=;kE*nqE4~D{PN3#E3UXAIQQIhn=Ca6R9Rr7!dflzIQ{g~ z-MJijPRDMa$0dkX{34Oa*LwX$Cd>{)Cloe;(EqKhsHX3d)AtQM;QHu8a( zk^p6WKIwZ&0w`|ED$$`shdNh*%vG4|x+sB)Qby{!hiUV3!6ll=rCe}jwOH)0^aEIL zB3QyI{Oa7fbAu^UrZ^XXwW2IgRfCP{h$D`0?%|}9PO4im5Q#KU6f9y}fJe(n4wI36 zE5KBCnaYG4x`UgQE>rOung*6i3;#N9+&Jggd=Nf>BtTi8Px@Xf0x*_~6;ttj|NZx_ zA%dGgah09m;;i=za9j1$58yp zo;>a$6>h{GT$BVBAB!tE5W$)_9&Q3RMyn6NpMLtOdoKp?C;`sp9#Y|kRy$ihiIIXP zuvAo3UijDp6|9NlVkiKnkHR{LkPldJbGe7wnZ_uz+UcrI6MMIa9f=4Ibg(9lO9RJZ za^!`iH?-QBs>FuJfhEKujs-4v+XvwTuFJ`lQsH1(h|&Y>GG*?8bNlF{kHWo;5?p%h zHwy0SwLL$n32vO#8d&b!`0?Xi=!L^CtFM5Qr_c4p7hiN~#flZdTW`JPz;52W*@ZK;tIFKyL(&$t~CHJpj8 zmbnGj%W9|9_B4W}s9?2p8^%>)Cfm*eYbX518*c>9J@;Jj!V511%a<>A`v`}S6-B>D z?%uuIwWhFU!Y2G9=ZdeS*R3qblvKnBhq;HSXjlmFjyTuQa0dDSP2E|tTILrtfJIoX z^~bi`ZD`gCOuz!zA2CP*Yuk2yK;~v}Q@;D|J7;k~v2$owxCcs({f4h3*WkmQqwI3Q z9p!_?YVotU1mD-1S_Qjk(V}44vSq<*uf3LNsaarco1)7|gDlAb=6uTntdi{fJLhZ$ z5LJ?E@ZnOKP16YtWjlI-6DCY>R_ps(QxUNE*Z1Cgui+&H zuGWtO6wQ%-vsY=8nw`CAU`@CfB8^vGdBs(HWr*%cdeFov+|wlHzA?G5u4tLz4L;Cx z!k9gs%M`PAz+$yn>Z`B5>MEinK$Z*WW~F6%`JmFj$>sl+DKT57>GLMGUw-*z7j?Y- z_S@m!_CzE?Gv8cdjOZHO%QM!8eLJib@>+zTq=mD zS_R8+Hy29$N-GQbze6slAL`{3N4{iO)OPam;lvq;9DUWw-`n zHs%=D)dbhSV2T3=r9<4*DpKf|_A#aVe5k&{_wQ4B48V3A-zQT_a7C1w zb_EOIQV01`I7nSJA*{QcGA-BqEp7o70gGs(6D44^(c@c=E6_{k%;uhi5q=KHOH{IA zD?5G0)$-Q)-L5Fb(K&6N8Y;C+%jG}-CNM7w7MDP?HEtN&a*0XF%+YFTc1!sAXO;MV z?C}AM|1CrCAnr|2L#38#xn>EdC|Gh_0&uNj@d3zcsr^?7RH4al;rCXkNgf+TSU*G= zu7QY<+n{8wG1B0v6F^qyvWbxqsoIX!fkGy(}!gzrjB~ITaeeZzH?yC`Y_oSFGih z36}3+u{3Yvz=pndRZ*)oh2LMLinkt~MSq8movJG}Urli1z$O8;+~n`K8$w&3CMk+u za41&bG<7r=6vyFsO}~TR=RC?#Mx|C)tmT@bCs-70^mG0NhubV^vbAIFisS4}_??a` z5pJ1a{m-LLU2wO+qFA_gLgYRmAC$j8&SHJp@k1BX)NCwE-2{u8;6}mvDhf6l8+u?- zQ?s!Rag(@NT?dOYE!R{OETUh6O27xp_MSa^nwpJmh?_Jb4lFk}PbE~Qa#^l{GqnO1 z(UZi)V`k_?!A5e1m1A!2q5Ez^|Ca!p0RB8ntdiv|0~Cs%+^4>WVH=C=ws z{5)1WJIQM4;HIOS20*)F)S1>E!}(dtv|R2GgMI|zs8z5gz+yd&Ap3xk0FPHTPboLx z2-tZ7%lBi&Z&Py05M8FgRf{m(ldMe3r3lbnNE5awSd`=%Ol+%pir>uz*aMsBucuC( z>TYORE`eq=DUVVu7Xg+I7jjetEMml%HDS1iIIxJyZpV%tfVy7kP5}g7lfwOXDqWuj z7U2%gIp>^GTmqsbTPcnatd;9k76!D)c z-6fFsDBYnnJIwE4Kkuix0>muL88c=$xE&HbLw&WEjc_4uk1%nv3&x4#7MS!_6fD9e z0G12B;DQTc!CKze*EeR{wrw-Dd7IK7l>QXvw+ZS@rP#{L#PONt$|RPutTs(OJv|id z_438_Cy+$pLcHE57ro%9-)%=s_hfR(HP>9@)~VqSF4w@)_p|ry+gEoDCfmj(FbjtZD{~Eki3uo7pE~7~Q=Db` ziT%8vGC8AkQJJxy?V8q23l6- zb|5IDGK(F-Zh{-iO|YoF{0XeZzpS!h@-lN@qmQ3Dm_Ubm`LIsi&TDuHs-6?jWVfOX%RHulh=jqhy&|WW7ANVXT7raqNqNMQqdW;9Ky) zqO=eUsb%)6vj6J6rf4BENHzDMn6gK$wuYrZ1{$ACO$uOojeyK7K;qQ{T`cQjg(s!c zSDH&exmo~%OE@4_gGS1Nomob~BDMj{I$SK73CHxHF&X4Qg^bk#mavm+@+GUKE%2-i zFm^V4F1Vq4*sYfX04xi3fa9VF{1Xd2@KI*Xbog|7LPi;4Eya>0OM<7LemdOKD8Z$M zdydjw+ta9AaFGxAtFOLluwXyuPO~(Mg?;+2vuGL|3{;lgY~{EdpQd=3&2d6a77E6# z_eFwBY_?d}*W_a&cf0+dKh;yH5~Kg~w|Hjq9+4^<@F) zqWLaX#l@3Y>U!_J_qsfB0kL1`5klGW14dO|!BpDk86;yKZT(2S|JUSi z`=Ya(sKTb0S|2TmiqID~hL#eEo5AP^aklC1yYCLJyY9NSJIuL(4?Xly@aUtDx_%U2 zVU+NojD_X0iE6t!B|uEja1Vp;?d^3>C`DOdlN1H+i^`FTk`mzSs;jPYo%lT1sNFtZ z9#ybl!Ghr7haV32G)ize0hZ6khG3>r+Nz!vPgG@=Yxuh?G2+WO9OGwITP+gYrjGGA zl~T3hhn1Gg-!W{kUH)j7(l<)(K@YW0RVM}{8A-%v5<7maHl1-=s(*bP=i3lG22w z+HiUqqrN$OEmpfgR?8y^GS$)k!4Nc|8z#o$5dS~WC`nj1lc9$Sd}^=V%{Sj1+?qtgMSxSq1Diignj)~N!78KAH{N)on^e+n zbbvc}^2sON=x*G|E^5_kQC5F$<XwAz**S2V zAAlw7;sKpJN3w0Os7kj$XUwU!m+7Jtrkh|x;b#knU2&=sF5QYh{gH2sWZkaYklw8_ z!4hs!aak8)7TB^XFke-=1^n$Ft>BZS<{|ugqV2@G;7l;FBDc_ z|5Yl*&qmd8xVjiOH-U9$jPjC~Ij1*mgxFV=U{TjBx7(^3Eb3Sbtv{&~U{RHDS%C3H z?PdRwz*2ct4i;4jmjxK{f5p@V3F>Ag{E zv8x5O)Nm_JmBK{=934*YhHy`Pf|@$@=#UQSkZKBo{{u^HJP9>g3z7f;002ovPDHLk FV1j9g503x< 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 49ac2cf80d..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,3 +1,4 @@ [General] Version: latest HitCircleOverlayAboveNumber: 0 +HitCirclePrefix: display \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index af02087d1a..30b0451a3b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true))); AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true))); AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true))); + AddStep("High combo index", () => SetContents(_ => testSingle(2, true, comboIndex: 15))); } [Test] @@ -66,12 +67,12 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true))); } - private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) + private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null, int comboIndex = 0) { var playfield = new TestOsuPlayfield(); for (double t = timeOffset; t < timeOffset + 60000; t += 2000) - playfield.Add(createSingle(circleSize, auto, t, positionOffset)); + playfield.Add(createSingle(circleSize, auto, t, positionOffset, comboIndex: comboIndex)); return playfield; } @@ -84,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Tests for (int i = 0; i <= 1000; i += 100) { - playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset)); + playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset, i / 100 - 1)); pos.X += 50; } return playfield; } - private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0) + private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0, int comboIndex = 0) { positionOffset ??= Vector2.Zero; @@ -99,6 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000 + timeOffset, Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value, + IndexInCurrentCombo = comboIndex, }; circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); From c9cb0561f7a2ac5117d9add6792e02b53014c1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 19:48:10 +0200 Subject: [PATCH 398/896] Move `maxSizePerGlyph` optional ctor param to init-only property I'm doing this as I'm about to add more similar properties to `LegacySpriteText` and I don't want to create a twenty-argument constructor monstrosity. --- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 3 ++- osu.Game/Skinning/LegacySpriteText.cs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 1de8fefaae..c01d28c8e1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -145,10 +145,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy return null; const float hitcircle_text_scale = 0.8f; - return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale) + return new LegacySpriteText(LegacyFont.HitCircle) { // stable applies a blanket 0.8x scale to hitcircle fonts Scale = new Vector2(hitcircle_text_scale), + MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale, }; case OsuSkinComponents.SpinnerBody: diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 7eb92126fa..a803ef8747 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -12,8 +12,9 @@ namespace osu.Game.Skinning { public sealed partial class LegacySpriteText : OsuSpriteText { + public Vector2? MaxSizePerGlyph { get; init; } + private readonly LegacyFont font; - private readonly Vector2? maxSizePerGlyph; private LegacyGlyphStore glyphStore = null!; @@ -21,10 +22,9 @@ namespace osu.Game.Skinning protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; - public LegacySpriteText(LegacyFont font, Vector2? maxSizePerGlyph = null) + public LegacySpriteText(LegacyFont font) { this.font = font; - this.maxSizePerGlyph = maxSizePerGlyph; Shadow = false; UseFullGlyphHeight = false; @@ -36,7 +36,7 @@ namespace osu.Game.Skinning Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); - glyphStore = new LegacyGlyphStore(skin, maxSizePerGlyph); + glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); From 99e590c8dd645c58b2fa6197e2f9bd7b003b9e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 20:10:36 +0200 Subject: [PATCH 399/896] Fix legacy sprite texts not matching stable with respect to fixed width stable's `pSpriteText` has a `TextConstantSpacing` flag, that is selectively enabled for some usages. In particular, these are: - mania combo counter (not yet implemented) - taiko combo counter (not yet implemented) - score counter - accuracy counter - scoreboard entries (not yet implemented) Everything else uses non-fixed-width fonts. Hilariously, `LegacySpinner` _tried_ to account for this by changing `Font` to have `fixedWidth: false` specified, only to fail to notice that `LegacySpriteText` changes `Font` in its BDL, making the property set do precisely nothing. For this reason, attempting to set `Font` on a `LegacySpriteText` will now throw. --- .../Skinning/Legacy/LegacySpinner.cs | 4 ++-- osu.Game/Skinning/LegacyAccuracyCounter.cs | 1 + osu.Game/Skinning/LegacyScoreCounter.cs | 1 + osu.Game/Skinning/LegacySpriteText.cs | 13 ++++++++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 28acb4a996..5a95eac0f1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre, Scale = new Vector2(SPRITE_SCALE), Y = SPINNER_TOP_OFFSET + 299, - }.With(s => s.Font = s.Font.With(fixedWidth: false)), + }, spmBackground = new Sprite { Anchor = Anchor.TopCentre, @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.TopRight, Scale = new Vector2(SPRITE_SCALE * 0.9f), Position = new Vector2(80, 448 + spm_hide_offset), - }.With(s => s.Font = s.Font.With(fixedWidth: false)), + }, } }); } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 326257c25f..ed12292eb3 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -25,6 +25,7 @@ namespace osu.Game.Skinning { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + FixedWidth = true, }; } } diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index d238369be1..a86f122836 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -28,6 +28,7 @@ namespace osu.Game.Skinning { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + FixedWidth = true, }; } } diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index a803ef8747..041a32e8de 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; @@ -13,6 +14,7 @@ namespace osu.Game.Skinning public sealed partial class LegacySpriteText : OsuSpriteText { public Vector2? MaxSizePerGlyph { get; init; } + public bool FixedWidth { get; init; } private readonly LegacyFont font; @@ -22,6 +24,15 @@ namespace osu.Game.Skinning protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; + // ReSharper disable once UnusedMember.Global + // being unused is the point here + public new FontUsage Font + { + get => base.Font; + set => throw new InvalidOperationException(@"Attempting to use this setter will not work correctly. " + + $@"Use specific init-only properties exposed by {nameof(LegacySpriteText)} instead."); + } + public LegacySpriteText(LegacyFont font) { this.font = font; @@ -33,7 +44,7 @@ namespace osu.Game.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { - Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: true); + base.Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: FixedWidth); Spacing = new Vector2(-skin.GetFontOverlap(font), 0); glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph); From 2f9b50172e0b0f261a94585d2c525405a77eaa48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 22:09:33 +0200 Subject: [PATCH 400/896] Add failing test coverage for video events affecting storyboard time bounds --- .../Formats/LegacyStoryboardDecoderTest.cs | 21 +++++++++++++++++++ .../video-background-events-ignored.osb | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 osu.Game.Tests/Resources/video-background-events-ignored.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 34ff8bfd84..647c0aed75 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -287,5 +287,26 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration)); } } + + [Test] + public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds() + { + var decoder = new LegacyStoryboardDecoder(); + + using var resStream = TestResources.OpenResource("video-background-events-ignored.osb"); + using var stream = new LineBufferedReader(resStream); + + var storyboard = decoder.Decode(stream); + + Assert.Multiple(() => + { + Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1)); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf()); + Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678)); + + Assert.That(storyboard.EarliestEventTime, Is.Null); + Assert.That(storyboard.LatestEventTime, Is.Null); + }); + } } } diff --git a/osu.Game.Tests/Resources/video-background-events-ignored.osb b/osu.Game.Tests/Resources/video-background-events-ignored.osb new file mode 100644 index 0000000000..7525b1fee9 --- /dev/null +++ b/osu.Game.Tests/Resources/video-background-events-ignored.osb @@ -0,0 +1,5 @@ +osu file format v14 + +[Events] +0,-1234,"BG.jpg",0,0 +Video,-5678,"Video.avi",0,0 \ No newline at end of file From 9ce2c1f49c37e63d3d6c18b8fb854ee3b4766adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 27 Oct 2023 22:10:03 +0200 Subject: [PATCH 401/896] Exclude video events from being accounted for when calculating storyboard time bounds Closes https://github.com/ppy/osu/issues/25263. In some circumstances, stable allows skipping twice if a particularly long storyboarded intro is being displayed: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameModes/Play/Player.cs#L1728-L1736 `AllowDoubleSkip` is calculated thus: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameModes/Play/Player.cs#L1761-L1770 and `leadInTime` is calculated thus: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameModes/Play/Player.cs#L1342-L1351 The key to watch out for here is `{first,last}EventTime`. `EventManager` will calculate it on-the-fly as it adds storyboard elements: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/Events/EventManager.cs#L253-L256 However, this pathway is only used for sprite, animation, sample, and break events. Video and background events use the following pathway: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameplayElements/Events/EventManager.cs#L368 Note that this particular overload does not mutate either bound. Which means that for the purposes of determining where a storyboard starts and ends temporally, a video event's start time is essentially ignored. To reflect that, add a clause that excludes video events from calculations of `{Earliest,Latest}EventTime`. --- osu.Game/Storyboards/Storyboard.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 21342831b0..a3137fe1b1 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -30,8 +30,11 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. + /// Video and background events are not included to match stable. /// - public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).MinBy(e => e.StartTime)?.StartTime; + public double? EarliestEventTime => Layers.SelectMany(l => l.Elements) + .Where(e => e is not StoryboardVideo) + .MinBy(e => e.StartTime)?.StartTime; /// /// Across all layers, find the latest point in time that a storyboard element ends at. @@ -39,9 +42,12 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. - /// Videos and samples return StartTime as their EndTIme. + /// Samples return StartTime as their EndTIme. + /// Video and background events are not included to match stable. /// - public double? LatestEventTime => Layers.SelectMany(l => l.Elements).MaxBy(e => e.GetEndTime())?.GetEndTime(); + public double? LatestEventTime => Layers.SelectMany(l => l.Elements) + .Where(e => e is not StoryboardVideo) + .MaxBy(e => e.GetEndTime())?.GetEndTime(); /// /// Depth of the currently front-most storyboard layer, excluding the overlay layer. From bac306879a4c2965b780c8588f97e95c24e354d0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 03:18:13 +0300 Subject: [PATCH 402/896] Minor reword on documentation --- osu.Game/Storyboards/Storyboard.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index a3137fe1b1..8c43b99702 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -30,6 +30,7 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. + /// Sample events use their start time as "end time" during this calculation. /// Video and background events are not included to match stable. /// public double? EarliestEventTime => Layers.SelectMany(l => l.Elements) @@ -42,7 +43,7 @@ namespace osu.Game.Storyboards /// /// /// This iterates all elements and as such should be used sparingly or stored locally. - /// Samples return StartTime as their EndTIme. + /// Sample events use their start time as "end time" during this calculation. /// Video and background events are not included to match stable. /// public double? LatestEventTime => Layers.SelectMany(l => l.Elements) From 51b7c97cabd8944cc7dfd840ae4a247d42efeca7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 06:07:49 +0300 Subject: [PATCH 403/896] Fix `TestScenePlayerMaxDimensions` bottlenecking CI --- .../Gameplay/TestScenePlayerMaxDimensions.cs | 68 +++---------------- 1 file changed, 10 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs index 68443b234b..741fc7d789 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -72,66 +72,18 @@ namespace osu.Game.Tests.Visual.Gameplay remove { } } + public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + var texture = base.GetTexture(componentName, wrapModeS, wrapModeT); + + if (texture != null) + texture.ScaleAdjust /= scale_factor; + + return texture; + } + public ISkin FindProvider(Func lookupFunction) => this; public IEnumerable AllSources => new[] { this }; - - protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) - => new UpscaledTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage)); - - private class UpscaledTextureLoaderStore : IResourceStore - { - private readonly IResourceStore? textureStore; - - public UpscaledTextureLoaderStore(IResourceStore? textureStore) - { - this.textureStore = textureStore; - } - - public void Dispose() - { - textureStore?.Dispose(); - } - - public TextureUpload Get(string name) - { - var textureUpload = textureStore?.Get(name); - - // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. - if (textureUpload == null) - return null!; - - return upscale(textureUpload); - } - - public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) - { - // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. - if (textureStore == null) - return null!; - - var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); - - if (textureUpload == null) - return null!; - - return await Task.Run(() => upscale(textureUpload), cancellationToken).ConfigureAwait(false); - } - - private TextureUpload upscale(TextureUpload textureUpload) - { - var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); - - // The original texture upload will no longer be returned or used. - textureUpload.Dispose(); - - image.Mutate(i => i.Resize(new Size(textureUpload.Width, textureUpload.Height) * scale_factor)); - return new TextureUpload(image); - } - - public Stream? GetStream(string name) => textureStore?.GetStream(name); - - public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty(); - } } } } From a53c0adae077847112830952a025df3de4cdfe68 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 06:56:12 +0300 Subject: [PATCH 404/896] Remove unused using directive --- .../Visual/Gameplay/TestScenePlayerMaxDimensions.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs index 741fc7d789..6665295e99 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -3,20 +3,14 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Skinning; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; namespace osu.Game.Tests.Visual.Gameplay { From 28e331deed1c27de3f6177b3fe779bf673cc4b97 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:29:46 +0300 Subject: [PATCH 405/896] Support displaying team seed in `TeamDisplay` --- .../TestSceneDrawableTournamentTeam.cs | 5 +-- .../TournamentTestScene.cs | 3 +- .../Gameplay/Components/TeamDisplay.cs | 34 ++++++++++++++++--- .../Gameplay/Components/TeamScoreDisplay.cs | 5 ++- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index a809d0747a..8a50cbbe13 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -21,6 +21,7 @@ namespace osu.Game.Tournament.Tests.Components { FlagName = { Value = "AU" }, FullName = { Value = "Australia" }, + Seed = { Value = "#5" }, Players = { new TournamentUser { Username = "ASecretBox" }, @@ -30,7 +31,7 @@ namespace osu.Game.Tournament.Tests.Components new TournamentUser { Username = "Parkes" }, new TournamentUser { Username = "Shiroha" }, new TournamentUser { Username = "Jordan The Bear" }, - } + }, }; var match = new TournamentMatch { Team1 = { Value = team } }; @@ -100,7 +101,7 @@ namespace osu.Game.Tournament.Tests.Components Cell(i).AddRange(new Drawable[] { new TournamentSpriteText { Text = "TeamDisplay" }, - new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6) + new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index f24fc61d85..4106556ee1 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tournament.Tests FlagName = { Value = "JP" }, FullName = { Value = "Japan" }, LastYearPlacing = { Value = 10 }, - Seed = { Value = "Low" }, + Seed = { Value = "#12" }, SeedingResults = { new SeedingResult @@ -140,6 +140,7 @@ namespace osu.Game.Tournament.Tests Acronym = { Value = "USA" }, FlagName = { Value = "US" }, FullName = { Value = "United States" }, + Seed = { Value = "#3" }, Players = { new TournamentUser { Username = "Hello" }, diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 3fdbbb5973..7e63b5b8db 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -14,9 +14,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { private readonly TeamScore score; - private readonly TournamentSpriteTextWithBackground teamText; + private readonly TournamentSpriteTextWithBackground teamNameText; + private readonly TournamentSpriteTextWithBackground teamSeedText; private readonly Bindable teamName = new Bindable("???"); + private readonly Bindable teamSeed = new Bindable(); private bool showScore; @@ -35,7 +37,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } - public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) + public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin, bool displaySeed) : base(team) { AutoSizeAxes = Axes.Both; @@ -95,11 +97,29 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } }, - teamText = new TournamentSpriteTextWithBackground + new FillFlowContainer { - Scale = new Vector2(0.5f), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), Origin = anchor, Anchor = anchor, + Children = new Drawable[] + { + teamNameText = new TournamentSpriteTextWithBackground + { + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + }, + teamSeedText = new TournamentSpriteTextWithBackground + { + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + Alpha = displaySeed ? 1 : 0, + } + } }, } }, @@ -117,9 +137,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components FinishTransforms(true); if (Team != null) + { teamName.BindTo(Team.FullName); + teamSeed.BindTo(Team.Seed); + } - teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true); + teamName.BindValueChanged(name => teamNameText.Text.Text = name.NewValue, true); + teamSeed.BindValueChanged(seed => teamSeedText.Text.Text = seed.NewValue, true); } private void updateDisplay() diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index c7fcfae602..9c2922d030 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -21,6 +21,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamDisplay? teamDisplay; + public readonly BindableBool DisplaySeed = new BindableBool(); + public bool ShowScore { get => teamDisplay?.ShowScore ?? false; @@ -48,6 +50,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentMatch.BindValueChanged(matchChanged); currentTeam.BindValueChanged(teamChanged); + DisplaySeed.BindValueChanged(_ => currentTeam.TriggerChange()); updateMatch(); } @@ -101,7 +104,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components InternalChildren = new Drawable[] { - teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), + teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0, DisplaySeed.Value), }; teamDisplay.ShowScore = wasShowingScores; From e2788a22b153ac5763c9d47a3534f719b4fde497 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:30:33 +0300 Subject: [PATCH 406/896] Add setting to configure team seed display --- osu.Game.Tournament/Models/LadderInfo.cs | 2 ++ .../Screens/Gameplay/Components/MatchHeader.cs | 5 +++++ .../Screens/Gameplay/GameplayScreen.cs | 12 +++++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs index 219a2a7bfb..f96dae8044 100644 --- a/osu.Game.Tournament/Models/LadderInfo.cs +++ b/osu.Game.Tournament/Models/LadderInfo.cs @@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models public Bindable AutoProgressScreens = new BindableBool(true); public Bindable SplitMapPoolByMods = new BindableBool(true); + + public Bindable DisplayTeamSeeds = new BindableBool(); } } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index 69f150c8ac..edf1d810ed 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -2,6 +2,7 @@ // 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.Tournament.Components; @@ -16,6 +17,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamScoreDisplay teamDisplay2 = null!; private DrawableTournamentHeaderLogo logo = null!; + public readonly BindableBool DisplaySeeds = new BindableBool(); + private bool showScores = true; public bool ShowScores @@ -88,11 +91,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, + DisplaySeed = { BindTarget = DisplaySeeds }, }, teamDisplay2 = new TeamScoreDisplay(TeamColour.Blue) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + DisplaySeed = { BindTarget = DisplaySeeds }, }, }; } diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 20188cc5dc..76d7f27421 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Screens.Gameplay private Drawable chroma = null!; [BackgroundDependencyLoader] - private void load(LadderInfo ladder, MatchIPCInfo ipc) + private void load(MatchIPCInfo ipc) { this.ipc = ipc; @@ -118,12 +118,18 @@ namespace osu.Game.Tournament.Screens.Gameplay LabelText = "Players per team", Current = LadderInfo.PlayersPerTeam, KeyboardStep = 1, - } + }, + new SettingsCheckbox + { + LabelText = "Display team seeds", + Current = LadderInfo.DisplayTeamSeeds, + }, } } }); - ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); + LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); + LadderInfo.DisplayTeamSeeds.BindValueChanged(v => header.DisplaySeeds.Value = v.NewValue, true); warmup.BindValueChanged(w => { From 832e30c31a3e68891c23a5f22ad156a3a6224fc4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:30:59 +0300 Subject: [PATCH 407/896] Adjust horizontal padding in tournament sprite text --- .../Components/TournamentSpriteTextWithBackground.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs index 97cb610021..ce118727cd 100644 --- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs @@ -29,8 +29,8 @@ namespace osu.Game.Tournament.Components { Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50), - Padding = new MarginPadding { Left = 10, Right = 20 }, - Text = text + Padding = new MarginPadding { Horizontal = 10 }, + Text = text, } }; } From 4371a1ab57a5bfb9549d948121bb15f5b1237ecd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:42:29 +0300 Subject: [PATCH 408/896] Move team seed setting from gameplay screen --- osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs | 9 ++------- osu.Game.Tournament/Screens/Setup/SetupScreen.cs | 6 ++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 76d7f27421..3e5ba90c52 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -49,7 +49,8 @@ namespace osu.Game.Tournament.Screens.Gameplay }, header = new MatchHeader { - ShowLogo = false + ShowLogo = false, + DisplaySeeds = { BindTarget = LadderInfo.DisplayTeamSeeds }, }, new Container { @@ -119,17 +120,11 @@ namespace osu.Game.Tournament.Screens.Gameplay Current = LadderInfo.PlayersPerTeam, KeyboardStep = 1, }, - new SettingsCheckbox - { - LabelText = "Display team seeds", - Current = LadderInfo.DisplayTeamSeeds, - }, } } }); LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); - LadderInfo.DisplayTeamSeeds.BindValueChanged(v => header.DisplaySeeds.Value = v.NewValue, true); warmup.BindValueChanged(w => { diff --git a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs index df1ce69c33..fed9d625ee 100644 --- a/osu.Game.Tournament/Screens/Setup/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs @@ -140,6 +140,12 @@ namespace osu.Game.Tournament.Screens.Setup Description = "Screens will progress automatically from gameplay -> results -> map pool", Current = LadderInfo.AutoProgressScreens, }, + new LabelledSwitchButton + { + Label = "Display team seeds", + Description = "Team seeds will display alongside each team at the top in gameplay/map pool screens.", + Current = LadderInfo.DisplayTeamSeeds, + }, }; } From 81c1634d4453ad31acceaacd48c5d59a868b73a7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 08:42:40 +0300 Subject: [PATCH 409/896] Display team seeds in map pool screen as well --- osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index f80f43bb77..15c6fdb2ae 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,6 +50,7 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader { ShowScores = true, + DisplaySeeds = { BindTarget = LadderInfo.DisplayTeamSeeds }, }, mapFlows = new FillFlowContainer> { From 7083c04c5923a9f4494bcdaadfc70de656a33c4d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 09:25:55 +0300 Subject: [PATCH 410/896] Refactor logic slightly to display team seed everywhere This change makes the team seed display in "team intro" screen as well. --- .../TestSceneDrawableTournamentTeam.cs | 2 +- .../Components/DrawableTeamSeed.cs | 39 +++++++++++++++++++ .../Components/DrawableTeamTitleWithHeader.cs | 12 +++++- .../Gameplay/Components/MatchHeader.cs | 5 --- .../Gameplay/Components/TeamDisplay.cs | 13 ++----- .../Gameplay/Components/TeamScoreDisplay.cs | 5 +-- .../Screens/Gameplay/GameplayScreen.cs | 1 - .../Screens/MapPool/MapPoolScreen.cs | 1 - 8 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 osu.Game.Tournament/Components/DrawableTeamSeed.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index 8a50cbbe13..a6eb482d02 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tournament.Tests.Components Cell(i).AddRange(new Drawable[] { new TournamentSpriteText { Text = "TeamDisplay" }, - new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6, true) + new TeamDisplay(team, TeamColour.Red, new Bindable(2), 6) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tournament/Components/DrawableTeamSeed.cs b/osu.Game.Tournament/Components/DrawableTeamSeed.cs new file mode 100644 index 0000000000..a79c63e979 --- /dev/null +++ b/osu.Game.Tournament/Components/DrawableTeamSeed.cs @@ -0,0 +1,39 @@ +// 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.Tournament.Models; + +namespace osu.Game.Tournament.Components +{ + public partial class DrawableTeamSeed : TournamentSpriteTextWithBackground + { + private readonly TournamentTeam? team; + + private IBindable seed = null!; + private Bindable displaySeed = null!; + + public DrawableTeamSeed(TournamentTeam? team) + { + this.team = team; + } + + [Resolved] + private LadderInfo ladder { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (team == null) + return; + + seed = team.Seed.GetBoundCopy(); + seed.BindValueChanged(s => Text.Text = s.NewValue, true); + + displaySeed = ladder.DisplayTeamSeeds.GetBoundCopy(); + displaySeed.BindValueChanged(v => Alpha = v.NewValue ? 1 : 0, true); + } + } +} diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs index 89f45fc1d3..fc4037d4e1 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -22,7 +22,17 @@ namespace osu.Game.Tournament.Components Children = new Drawable[] { new DrawableTeamHeader(colour), - new DrawableTeamTitle(team), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new DrawableTeamTitle(team), + new DrawableTeamSeed(team), + } + } } }; } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs index edf1d810ed..69f150c8ac 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchHeader.cs @@ -2,7 +2,6 @@ // 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.Tournament.Components; @@ -17,8 +16,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamScoreDisplay teamDisplay2 = null!; private DrawableTournamentHeaderLogo logo = null!; - public readonly BindableBool DisplaySeeds = new BindableBool(); - private bool showScores = true; public bool ShowScores @@ -91,13 +88,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - DisplaySeed = { BindTarget = DisplaySeeds }, }, teamDisplay2 = new TeamScoreDisplay(TeamColour.Blue) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - DisplaySeed = { BindTarget = DisplaySeeds }, }, }; } diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 7e63b5b8db..49fbc64397 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -15,10 +15,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private readonly TeamScore score; private readonly TournamentSpriteTextWithBackground teamNameText; - private readonly TournamentSpriteTextWithBackground teamSeedText; private readonly Bindable teamName = new Bindable("???"); - private readonly Bindable teamSeed = new Bindable(); private bool showScore; @@ -37,7 +35,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } - public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin, bool displaySeed) + public TeamDisplay(TournamentTeam? team, TeamColour colour, Bindable currentTeamScore, int pointsToWin) : base(team) { AutoSizeAxes = Axes.Both; @@ -112,13 +110,12 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components Origin = anchor, Anchor = anchor, }, - teamSeedText = new TournamentSpriteTextWithBackground + new DrawableTeamSeed(Team) { Scale = new Vector2(0.5f), Origin = anchor, Anchor = anchor, - Alpha = displaySeed ? 1 : 0, - } + }, } }, } @@ -137,13 +134,9 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components FinishTransforms(true); if (Team != null) - { teamName.BindTo(Team.FullName); - teamSeed.BindTo(Team.Seed); - } teamName.BindValueChanged(name => teamNameText.Text.Text = name.NewValue, true); - teamSeed.BindValueChanged(seed => teamSeedText.Text.Text = seed.NewValue, true); } private void updateDisplay() diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs index 9c2922d030..c7fcfae602 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamScoreDisplay.cs @@ -21,8 +21,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private TeamDisplay? teamDisplay; - public readonly BindableBool DisplaySeed = new BindableBool(); - public bool ShowScore { get => teamDisplay?.ShowScore ?? false; @@ -50,7 +48,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components currentMatch.BindValueChanged(matchChanged); currentTeam.BindValueChanged(teamChanged); - DisplaySeed.BindValueChanged(_ => currentTeam.TriggerChange()); updateMatch(); } @@ -104,7 +101,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components InternalChildren = new Drawable[] { - teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0, DisplaySeed.Value), + teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), }; teamDisplay.ShowScore = wasShowingScores; diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 3e5ba90c52..b2152eaf3d 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -50,7 +50,6 @@ namespace osu.Game.Tournament.Screens.Gameplay header = new MatchHeader { ShowLogo = false, - DisplaySeeds = { BindTarget = LadderInfo.DisplayTeamSeeds }, }, new Container { diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 15c6fdb2ae..f80f43bb77 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -50,7 +50,6 @@ namespace osu.Game.Tournament.Screens.MapPool new MatchHeader { ShowScores = true, - DisplaySeeds = { BindTarget = LadderInfo.DisplayTeamSeeds }, }, mapFlows = new FillFlowContainer> { From e76a5f9419276d8eaa8bea22c899fd1aa651c80b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 10:18:15 +0300 Subject: [PATCH 411/896] Fix failing tests --- .../Components/TestSceneDrawableTournamentTeam.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs index a6eb482d02..43adcc61bf 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneDrawableTournamentTeam.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.Tests.Visual; @@ -14,9 +15,14 @@ namespace osu.Game.Tournament.Tests.Components { public partial class TestSceneDrawableTournamentTeam : OsuGridTestScene { + [Cached] + protected LadderInfo Ladder { get; private set; } = new LadderInfo(); + public TestSceneDrawableTournamentTeam() : base(4, 3) { + AddToggleStep("toggle seed view", v => Ladder.DisplayTeamSeeds.Value = v); + var team = new TournamentTeam { FlagName = { Value = "AU" }, From cfc0520481df18553b7252f1f63149ee89e4c9e9 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 28 Oct 2023 12:13:13 +0200 Subject: [PATCH 412/896] Add failing test --- .../SongSelect/TestScenePlaySongSelect.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6737ec9739..7313bde8fe 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -13,6 +13,7 @@ using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -1111,6 +1112,23 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("0 matching shown", () => songSelect.ChildrenOfType().Single().InformationalText == "0 matches"); } + [Test] + public void TestCutInFilterTextBox() + { + createSongSelect(); + + AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType().First().Text = "nonono"); + AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll)); + AddStep("press ctrl-x", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.X); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType().First().Text, () => Is.Empty); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); From 366e41f11182c1220b2c8e33bc74172fa7543986 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sat, 28 Oct 2023 12:23:23 +0200 Subject: [PATCH 413/896] Use local workaround instead of disabling clipboard entirely --- osu.Game/Screens/Select/FilterControl.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index b7dc18e46a..c15bd76ef8 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -9,6 +9,7 @@ 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.Events; using osu.Framework.Localisation; using osu.Game.Collections; @@ -23,6 +24,7 @@ using osu.Game.Rulesets; using osu.Game.Screens.Select.Filter; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.Select { @@ -254,9 +256,6 @@ namespace osu.Game.Screens.Select public OsuSpriteText FilterText { get; private set; } - // clipboard is disabled because one of the "cut" platform key bindings (shift-delete) conflicts with the beatmap deletion action. - protected override bool AllowClipboardExport => false; - public FilterControlTextBox() { Height += filter_text_size; @@ -277,6 +276,15 @@ namespace osu.Game.Screens.Select Colour = colours.Yellow }); } + + public override bool OnPressed(KeyBindingPressEvent e) + { + // the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action. + if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete)) + return false; + + return base.OnPressed(e); + } } } } From c38c8e933ab0aba67d7a36ccff1413cde465dd8e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 28 Oct 2023 16:52:33 +0300 Subject: [PATCH 414/896] Change tournament date text box parsing to use invariant culture info --- osu.Game.Tournament/Components/DateTextBox.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index ab643a5cb5..dd70d5856d 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Globalization; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -23,13 +24,13 @@ namespace osu.Game.Tournament.Components base.Current = new Bindable(string.Empty); current.BindValueChanged(dto => - base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); + base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", DateTimeFormatInfo.InvariantInfo), true); ((OsuTextBox)Control).OnCommit += (sender, _) => { try { - current.Value = DateTimeOffset.Parse(sender.Text); + current.Value = DateTimeOffset.Parse(sender.Text, DateTimeFormatInfo.InvariantInfo); } catch { From d877536dc0ec8d1f8f1889be73740a58ef29f869 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 29 Oct 2023 01:03:38 +0300 Subject: [PATCH 415/896] Select all text content in `SearchTextBox` on focus --- osu.Game/Graphics/UserInterface/SearchTextBox.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index a2e0ab6482..b554c2bbd8 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -18,6 +18,12 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = HomeStrings.SearchPlaceholder; } + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + SelectAll(); + } + public override bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) From 31c6973bb646272b3ddcb2b3405d1802c8bf770c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 29 Oct 2023 01:03:45 +0300 Subject: [PATCH 416/896] Add test coverage --- .../UserInterface/TestSceneSearchTextBox.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs new file mode 100644 index 0000000000..153525d24a --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs @@ -0,0 +1,38 @@ +// 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.Graphics; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public partial class TestSceneSearchTextBox : OsuTestScene + { + private SearchTextBox textBox = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = textBox = new SearchTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 400, + Scale = new Vector2(2f), + HoldFocus = true, + }; + }); + + [Test] + public void TestSelectionOnFocus() + { + AddStep("set text", () => textBox.Text = "some text"); + AddAssert("no text selected", () => textBox.SelectedText == string.Empty); + AddStep("hide text box", () => textBox.Hide()); + AddStep("show text box", () => textBox.Show()); + AddAssert("search text selected", () => textBox.SelectedText == textBox.Text); + } + } +} From ec9ae12bbd880f2f9a1c31adca3d09e516d0dbb4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 29 Oct 2023 01:43:49 +0300 Subject: [PATCH 417/896] Update API response model to accept array of tournament banners --- .../Online/TestSceneUserProfileOverlay.cs | 27 +++++++++++++++---- .../Online/API/Requests/Responses/APIUser.cs | 5 ++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index b57b0b7312..a321a194a9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -121,12 +121,29 @@ namespace osu.Game.Tests.Visual.Online Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray() }, }, - TournamentBanner = new TournamentBanner + TournamentBanners = new[] { - Id = 13926, - TournamentId = 35, - ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg", - Image = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US@2x.jpg", + new TournamentBanner + { + Id = 15588, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg" + }, + new TournamentBanner + { + Id = 15589, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg" + }, + new TournamentBanner + { + Id = 15590, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg" + } }, Badges = new[] { diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index d9208d0662..7c4093006d 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -234,9 +234,8 @@ namespace osu.Game.Online.API.Requests.Responses set => Statistics.RankHistory = value; } - [JsonProperty(@"active_tournament_banner")] - [CanBeNull] - public TournamentBanner TournamentBanner; + [JsonProperty(@"active_tournament_banners")] + public TournamentBanner[] TournamentBanners; [JsonProperty("badges")] public Badge[] Badges; From 922ad80cfc9faf6fd63a0a5f01a266247924c4f8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 29 Oct 2023 01:44:21 +0300 Subject: [PATCH 418/896] Update user profile overlay to show more than one tournament banner --- .../Profile/Header/BannerHeaderContainer.cs | 15 ++++++++------- .../Header/Components/DrawableTournamentBanner.cs | 9 ++++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs index 8e6648dc4b..7ed58200ec 100644 --- a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.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.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,7 +12,7 @@ using osu.Game.Overlays.Profile.Header.Components; namespace osu.Game.Overlays.Profile.Header { - public partial class BannerHeaderContainer : CompositeDrawable + public partial class BannerHeaderContainer : FillFlowContainer { public readonly Bindable User = new Bindable(); @@ -19,9 +20,9 @@ namespace osu.Game.Overlays.Profile.Header private void load() { Alpha = 0; - RelativeSizeAxes = Axes.Both; - FillMode = FillMode.Fit; - FillAspectRatio = 1000 / 60f; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; } protected override void LoadComplete() @@ -40,13 +41,13 @@ namespace osu.Game.Overlays.Profile.Header ClearInternal(); - var banner = user?.TournamentBanner; + var banners = user?.TournamentBanners; - if (banner != null) + if (banners?.Length > 0) { Show(); - LoadComponentAsync(new DrawableTournamentBanner(banner), AddInternal, cancellationTokenSource.Token); + LoadComponentsAsync(banners.Select(b => new DrawableTournamentBanner(b)), AddRangeInternal, cancellationTokenSource.Token); } else { diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs index 26d333ff95..c099009ca4 100644 --- a/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs +++ b/osu.Game/Overlays/Profile/Header/Components/DrawableTournamentBanner.cs @@ -15,12 +15,13 @@ namespace osu.Game.Overlays.Profile.Header.Components [LongRunningLoad] public partial class DrawableTournamentBanner : OsuClickableContainer { + private const float banner_aspect_ratio = 60 / 1000f; private readonly TournamentBanner banner; public DrawableTournamentBanner(TournamentBanner banner) { this.banner = banner; - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; } [BackgroundDependencyLoader] @@ -41,6 +42,12 @@ namespace osu.Game.Overlays.Profile.Header.Components this.FadeInFromZero(200); } + protected override void Update() + { + base.Update(); + Height = DrawWidth * banner_aspect_ratio; + } + public override LocalisableString TooltipText => "view in browser"; } } From 204ebfade7ecaf5a426bfc21a6b89cfa6393fed8 Mon Sep 17 00:00:00 2001 From: Rowe Wilson Frederisk Holme Date: Sun, 29 Oct 2023 21:25:15 +0800 Subject: [PATCH 419/896] Small update to README of Templates --- Templates/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Templates/README.md b/Templates/README.md index cf25a89273..28aaee3290 100644 --- a/Templates/README.md +++ b/Templates/README.md @@ -7,7 +7,7 @@ Templates for use when creating osu! dependent projects. Create a fully-testable ```bash # install (or update) templates package. # this only needs to be done once -dotnet new -i ppy.osu.Game.Templates +dotnet new install ppy.osu.Game.Templates # create an empty freeform ruleset dotnet new ruleset -n MyCoolRuleset From af10dbb76c4b2ab6217655f7b0eeee124acc3635 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Oct 2023 06:19:52 +0300 Subject: [PATCH 420/896] Add test case with many tournament banners --- .../Online/TestSceneUserProfileHeader.cs | 266 +++++++++++++++++- 1 file changed, 265 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 4f28baa849..c9e5a3315c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -5,8 +5,10 @@ using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -28,7 +30,14 @@ namespace osu.Game.Tests.Visual.Online [SetUpSteps] public void SetUpSteps() { - AddStep("create header", () => Child = header = new ProfileHeader()); + AddStep("create header", () => + { + Child = new OsuScrollContainer(Direction.Vertical) + { + RelativeSizeAxes = Axes.Both, + Child = header = new ProfileHeader() + }; + }); } [Test] @@ -136,5 +145,260 @@ namespace osu.Game.Tests.Visual.Online PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" } }, new OsuRuleset().RulesetInfo)); } + + [Test] + public void TestManyTournamentBanners() + { + AddStep("Show user w/ many tournament banners", () => header.User.Value = new UserProfileData(new APIUser + { + Id = 728, + Username = "Certain Guy", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + Statistics = new UserStatistics + { + IsRanked = false, + // web will sometimes return non-empty rank history even for unranked users. + RankHistory = new APIRankHistory + { + Mode = @"osu", + Data = Enumerable.Range(2345, 85).ToArray() + }, + }, + TournamentBanners = new[] + { + new TournamentBanner + { + Id = 15329, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg" + }, + new TournamentBanner + { + Id = 15588, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg" + }, + new TournamentBanner + { + Id = 15589, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg" + }, + new TournamentBanner + { + Id = 15590, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg" + }, + new TournamentBanner + { + Id = 15591, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP@2x.jpg" + }, + new TournamentBanner + { + Id = 15592, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU@2x.jpg" + }, + new TournamentBanner + { + Id = 15593, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR@2x.jpg" + }, + new TournamentBanner + { + Id = 15594, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ@2x.jpg" + }, + new TournamentBanner + { + Id = 15595, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH@2x.jpg" + }, + new TournamentBanner + { + Id = 15596, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW@2x.jpg" + }, + new TournamentBanner + { + Id = 15603, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID@2x.jpg" + }, + new TournamentBanner + { + Id = 15604, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ@2x.jpg" + }, + new TournamentBanner + { + Id = 15605, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR@2x.jpg" + }, + new TournamentBanner + { + Id = 15606, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR@2x.jpg" + }, + new TournamentBanner + { + Id = 15607, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg" + }, + new TournamentBanner + { + Id = 15639, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX@2x.jpg" + }, + new TournamentBanner + { + Id = 15640, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU@2x.jpg" + }, + new TournamentBanner + { + Id = 15641, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT@2x.jpg" + }, + new TournamentBanner + { + Id = 15642, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA@2x.jpg" + }, + new TournamentBanner + { + Id = 15643, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL@2x.jpg" + }, + new TournamentBanner + { + Id = 15644, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI@2x.jpg" + }, + new TournamentBanner + { + Id = 15645, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO@2x.jpg" + }, + new TournamentBanner + { + Id = 15646, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG@2x.jpg" + }, + new TournamentBanner + { + Id = 15647, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE@2x.jpg" + }, + new TournamentBanner + { + Id = 15648, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES@2x.jpg" + }, + new TournamentBanner + { + Id = 15649, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE@2x.jpg" + }, + new TournamentBanner + { + Id = 15650, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA@2x.jpg" + }, + new TournamentBanner + { + Id = 15651, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO@2x.jpg" + }, + new TournamentBanner + { + Id = 15652, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB@2x.jpg" + }, + new TournamentBanner + { + Id = 15653, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US@2x.jpg" + }, + new TournamentBanner + { + Id = 15654, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg" + }, + new TournamentBanner + { + Id = 15655, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR@2x.jpg" + }, + new TournamentBanner + { + Id = 15686, + TournamentId = 41, + ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg", + Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg" + } + } + }, new OsuRuleset().RulesetInfo)); + } } } From 984c30ded662bc6091c980dc6d6fcf1da880479e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Oct 2023 06:20:13 +0300 Subject: [PATCH 421/896] Load each tournament banner as soon as it is loaded --- .../Overlays/Profile/Header/BannerHeaderContainer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs index 7ed58200ec..424ab4a529 100644 --- a/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BannerHeaderContainer.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; @@ -47,7 +46,15 @@ namespace osu.Game.Overlays.Profile.Header { Show(); - LoadComponentsAsync(banners.Select(b => new DrawableTournamentBanner(b)), AddRangeInternal, cancellationTokenSource.Token); + for (int index = 0; index < banners.Length; index++) + { + int displayIndex = index; + LoadComponentAsync(new DrawableTournamentBanner(banners[index]), asyncBanner => + { + // load in stable order regardless of async load order. + Insert(displayIndex, asyncBanner); + }, cancellationTokenSource.Token); + } } else { From c7bc8e686543e4fe6e823d06b5d79be144d8ad6e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Oct 2023 06:41:01 +0300 Subject: [PATCH 422/896] Move behaviour to settings search text box only --- .../Visual/Settings/TestSceneSettingsPanel.cs | 11 +++++++++++ .../Graphics/UserInterface/SearchTextBox.cs | 6 ------ osu.Game/Overlays/SettingsPanel.cs | 2 +- osu.Game/Overlays/SettingsSearchTextBox.cs | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Overlays/SettingsSearchTextBox.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs index 24c2eee783..69e489b247 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsPanel.cs @@ -140,6 +140,17 @@ namespace osu.Game.Tests.Visual.Settings AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType().FirstOrDefault()?.HasFocus == true); } + [Test] + public void TestSearchTextBoxSelectedOnShow() + { + SearchTextBox searchTextBox = null!; + + AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType().First()).Current.Value = "some text"); + AddAssert("no text selected", () => searchTextBox.SelectedText == string.Empty); + AddRepeatStep("toggle visibility", () => settings.ToggleVisibility(), 2); + AddAssert("search text selected", () => searchTextBox.SelectedText == searchTextBox.Current.Value); + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index b554c2bbd8..a2e0ab6482 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -18,12 +18,6 @@ namespace osu.Game.Graphics.UserInterface PlaceholderText = HomeStrings.SearchPlaceholder; } - protected override void OnFocus(FocusEvent e) - { - base.OnFocus(e); - SelectAll(); - } - public override bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 2517a58491..3bac6c400f 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -135,7 +135,7 @@ namespace osu.Game.Overlays }, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Child = searchTextBox = new SeekLimitedSearchTextBox + Child = searchTextBox = new SettingsSearchTextBox { RelativeSizeAxes = Axes.X, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/SettingsSearchTextBox.cs b/osu.Game/Overlays/SettingsSearchTextBox.cs new file mode 100644 index 0000000000..bafa6e26eb --- /dev/null +++ b/osu.Game/Overlays/SettingsSearchTextBox.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. + +#nullable disable +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays +{ + public partial class SettingsSearchTextBox : SeekLimitedSearchTextBox + { + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + SelectAll(); + } + } +} From d90f29a5ff4a4618955b7ac7c4e743d39b4aa03a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:07:26 +0900 Subject: [PATCH 423/896] Improve log output surrounding score submission --- osu.Game/Screens/Play/SubmittingPlayer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 5fa6508a31..a75546f835 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -188,7 +188,10 @@ namespace osu.Game.Screens.Play { // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure). if (token == null) + { + Logger.Log("No token, skipping score submission"); return Task.CompletedTask; + } if (scoreSubmissionSource != null) return scoreSubmissionSource.Task; @@ -197,6 +200,8 @@ namespace osu.Game.Screens.Play if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0)) return Task.CompletedTask; + Logger.Log($"Beginning score submission (token:{token.Value})..."); + scoreSubmissionSource = new TaskCompletionSource(); var request = CreateSubmissionRequest(score, token.Value); @@ -206,11 +211,12 @@ namespace osu.Game.Screens.Play score.ScoreInfo.Position = s.Position; scoreSubmissionSource.SetResult(true); + Logger.Log($"Score submission completed! (token:{token.Value} id:{s.ID})"); }; request.Failure += e => { - Logger.Error(e, $"Failed to submit score ({e.Message})"); + Logger.Error(e, $"Failed to submit score (token:{token.Value}): {e.Message}"); scoreSubmissionSource.SetResult(false); }; From a8c3f598457ee9079ba97d2f381f75f7774890dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:07:35 +0900 Subject: [PATCH 424/896] Clean up type display for web requests in logs --- osu.Game/Online/API/APIRequest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index cd6e8df754..6b6b222043 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -7,6 +7,7 @@ using System; using System.Globalization; using JetBrains.Annotations; using Newtonsoft.Json; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.IO.Network; using osu.Framework.Logging; using osu.Game.Extensions; @@ -46,7 +47,7 @@ namespace osu.Game.Online.API if (WebRequest != null) { Response = ((OsuJsonWebRequest)WebRequest).ResponseObject; - Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); + Logger.Log($"{GetType().ReadableName()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network); } } From a91b704d21df8adeca8068dd448d2ac9424010c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:10:10 +0900 Subject: [PATCH 425/896] Fix some new nullable inspections --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++------ osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 2 ++ osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs | 2 ++ osu.Game/Screens/Play/SquareGraph.cs | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index c62659d67a..3cb9b96090 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -49,13 +49,9 @@ namespace osu.Game.Rulesets.Osu.Objects set { path.ControlPoints.Clear(); - path.ExpectedDistance.Value = null; + path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type))); - if (value != null) - { - path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type))); - path.ExpectedDistance.Value = value.ExpectedDistance.Value; - } + path.ExpectedDistance.Value = value.ExpectedDistance.Value; } } diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index 158c3c102c..3ed7558bcb 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -5,6 +5,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Edit private SelectionState state; + [CanBeNull] public event Action StateChanged; public SelectionState State diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 1506b884b4..85ea881006 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -166,6 +166,8 @@ namespace osu.Game.Screens.Backgrounds public override void Add(Drawable drawable) { + ArgumentNullException.ThrowIfNull(drawable); + if (drawable is Background) throw new InvalidOperationException($"Use {nameof(Background)} to set a background."); diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index b53e86a41b..0c7b485755 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -190,6 +191,7 @@ namespace osu.Game.Screens.Play private const float padding = 2; public const float WIDTH = cube_size + padding; + [CanBeNull] public event Action StateChanged; private readonly List drawableRows = new List(); From 96dd7b3333014cb5e8e404d129b163de1f72f6e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:44:16 +0900 Subject: [PATCH 426/896] Update the last played date of a beatmap when importing a replay by the local user --- osu.Game.Tests/ImportTest.cs | 4 ++ osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 59 +++++++++++++++++++++ osu.Game/Online/API/DummyAPIAccess.cs | 2 +- osu.Game/Scoring/ScoreImporter.cs | 3 ++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index 3f0f8a4f14..27b8d3f21e 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Game.Database; +using osu.Game.Online.API; using osu.Game.Tests.Resources; namespace osu.Game.Tests @@ -46,12 +47,15 @@ namespace osu.Game.Tests public partial class TestOsuGameBase : OsuGameBase { public RealmAccess Realm => Dependencies.Get(); + public new IAPIProvider API => base.API; private readonly bool withBeatmap; public TestOsuGameBase(bool withBeatmap) { this.withBeatmap = withBeatmap; + + base.API = new DummyAPIAccess(); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 892ceea185..d1bacaaf69 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Game.IO.Archives; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -67,6 +68,64 @@ namespace osu.Game.Tests.Scores.IO } } + [TestCase(false)] + [TestCase(true)] + public void TestLastPlayedUpdate(bool isLocalUser) + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + if (!isLocalUser) + osu.API.Logout(); + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var beatmapInfo = beatmap.Beatmaps.First(); + + DateTimeOffset replayDate = DateTimeOffset.Now; + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser + { + Username = "Test user", + Id = DummyAPIAccess.DUMMY_USER_ID, + }, + Date = replayDate, + OnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmapInfo + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + + if (isLocalUser) + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(replayDate)); + else + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.Null); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportMods() { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 2764247f5c..d585124db6 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -112,7 +112,7 @@ namespace osu.Game.Online.API LocalUser.Value = new APIUser { Username = username, - Id = 1001, + Id = DUMMY_USER_ID, }; state.Value = APIState.Online; diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 7473d887c3..32a528f218 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -87,6 +87,9 @@ namespace osu.Game.Scoring if (!model.Ruleset.IsManaged) model.Ruleset = realm.Find(model.Ruleset.ShortName)!; + if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed)) + model.BeatmapInfo.LastPlayed = model.Date; + // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed). // Under no circumstance do we want these to be written to realm as null. ArgumentNullException.ThrowIfNull(model.BeatmapInfo); From c1c8e2968f1f620851c4e608043051f07b944d8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 15:46:09 +0900 Subject: [PATCH 427/896] Move operation to after user population --- osu.Game/Scoring/ScoreImporter.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/ScoreImporter.cs b/osu.Game/Scoring/ScoreImporter.cs index 32a528f218..b216c0897e 100644 --- a/osu.Game/Scoring/ScoreImporter.cs +++ b/osu.Game/Scoring/ScoreImporter.cs @@ -87,9 +87,6 @@ namespace osu.Game.Scoring if (!model.Ruleset.IsManaged) model.Ruleset = realm.Find(model.Ruleset.ShortName)!; - if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed)) - model.BeatmapInfo.LastPlayed = model.Date; - // These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed). // Under no circumstance do we want these to be written to realm as null. ArgumentNullException.ThrowIfNull(model.BeatmapInfo); @@ -185,6 +182,12 @@ namespace osu.Game.Scoring base.PostImport(model, realm, parameters); populateUserDetails(model); + + Debug.Assert(model.BeatmapInfo != null); + + // This needs to be run after user detail population to ensure we have a valid user id. + if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed)) + model.BeatmapInfo.LastPlayed = model.Date; } /// From 4ae9b40a7806d6637840c25349282f07ef6d66e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 16:35:14 +0900 Subject: [PATCH 428/896] 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 bf0a1fcbad..4f03f398d5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From 0dfb41b7966de7823982f3f18b2a5adfa38ac1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 09:28:37 +0100 Subject: [PATCH 429/896] Add test coverage for not updating `LastPlayed` due to newer plays --- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index d1bacaaf69..dd724d268e 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -11,6 +11,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -126,6 +128,58 @@ namespace osu.Game.Tests.Scores.IO } } + [Test] + public void TestLastPlayedNotUpdatedDueToNewerPlays() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = LoadOsuIntoHost(host, true); + + var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely(); + var beatmapInfo = beatmap.Beatmaps.First(); + + var realmAccess = osu.Dependencies.Get(); + realmAccess.Write(r => r.Find(beatmapInfo.ID)!.LastPlayed = new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero)); + + var toImport = new ScoreInfo + { + Rank = ScoreRank.B, + TotalScore = 987654, + Accuracy = 0.8, + MaxCombo = 500, + Combo = 250, + User = new APIUser + { + Username = "Test user", + Id = DummyAPIAccess.DUMMY_USER_ID, + }, + Date = new DateTimeOffset(2023, 10, 27, 0, 0, 0, TimeSpan.Zero), + OnlineID = 12345, + Ruleset = new OsuRuleset().RulesetInfo, + BeatmapInfo = beatmapInfo + }; + + var imported = LoadScoreIntoOsu(osu, toImport); + + Assert.AreEqual(toImport.Rank, imported.Rank); + Assert.AreEqual(toImport.TotalScore, imported.TotalScore); + Assert.AreEqual(toImport.Accuracy, imported.Accuracy); + Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo); + Assert.AreEqual(toImport.User.Username, imported.User.Username); + Assert.AreEqual(toImport.Date, imported.Date); + Assert.AreEqual(toImport.OnlineID, imported.OnlineID); + + Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero))); + } + finally + { + host.Exit(); + } + } + } + [Test] public void TestImportMods() { From 39abb8e4085d17bc8ff64a80f19e268a3d4a5b7f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 30 Oct 2023 11:54:19 +0300 Subject: [PATCH 430/896] Only run "select all on focus" behaviour on desktop platforms --- osu.Game/Overlays/SettingsSearchTextBox.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsSearchTextBox.cs b/osu.Game/Overlays/SettingsSearchTextBox.cs index bafa6e26eb..84cff1b508 100644 --- a/osu.Game/Overlays/SettingsSearchTextBox.cs +++ b/osu.Game/Overlays/SettingsSearchTextBox.cs @@ -12,7 +12,11 @@ namespace osu.Game.Overlays protected override void OnFocus(FocusEvent e) { base.OnFocus(e); - SelectAll(); + + // on mobile platforms, focus is not held by the search text box, and the select all feature + // will not make sense on it, and might annoy the user when they try to focus manually. + if (HoldFocus) + SelectAll(); } } } From 6be02966b9e79dfbb94ad372b932decc78ee65f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 18:06:09 +0900 Subject: [PATCH 431/896] Add test coverage of failing context menu display --- .../Editing/TestSceneTimelineSelection.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 50eeb9a54b..0051488029 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -7,8 +7,10 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; @@ -44,6 +46,47 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestContextMenuWithObjectBehind() + { + TimelineHitObjectBlueprint blueprint; + + AddStep("add object", () => + { + EditorBeatmap.Add(new HitCircle { StartTime = 3000 }); + }); + + AddStep("enter slider placement", () => + { + InputManager.Key(Key.Number3); + InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre); + }); + + AddStep("start conflicting slider", () => + { + InputManager.Click(MouseButton.Left); + + blueprint = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(blueprint.ScreenSpaceDrawQuad.TopLeft - new Vector2(10, 0)); + }); + + AddStep("end conflicting slider", () => + { + InputManager.Click(MouseButton.Right); + }); + + AddStep("click object", () => + { + InputManager.Key(Key.Number1); + blueprint = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(blueprint); + InputManager.Click(MouseButton.Left); + }); + + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddAssert("context menu open", () => this.ChildrenOfType().SingleOrDefault()?.State == MenuState.Open); + } + [Test] public void TestNudgeSelection() { From 57d88a0ac459398a14aafd8923238ce1bc012e7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 17:46:04 +0900 Subject: [PATCH 432/896] Fix right clicks on timeline objects potentially getting eaten by playfield area `SelectionHandler` is receiving input from anywhere out of necessity: https://github.com/ppy/osu/blob/19f892687a0607afbe4e0d010366dc2a66236073/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs#L119-L125 Also important is that `BlueprintContainer` will selectively not block right clicks to make sure they fall through to the `ContextMenuContainer`: https://github.com/ppy/osu/blob/19f892687a0607afbe4e0d010366dc2a66236073/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs#L122-L126 But because the whole editor is sharing a `ContextMenuContainer` and it's at a higher level than both components, we observe here the playfield's `SelectionHandler` intercepting the right click before it can reach the `ContextMenuContainer`. The fix here is similar to what we're already doing in `TimelineBlueprintContaienr`. --- .../Compose/Components/ComposeBlueprintContainer.cs | 5 ++++- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 13 +++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index c8cfac454a..ba570a9251 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -40,11 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components public PlacementBlueprint CurrentPlacement { get; private set; } + [Resolved] + private EditorScreenWithTimeline editorScreen { get; set; } + /// /// Positional input must be received outside the container's bounds, /// in order to handle composer blueprints which are partially offscreen. /// - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen.MainContent.ReceivePositionalInputAt(screenSpacePos); public ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index ea2790b50a..e1ec1ad4ac 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -11,13 +11,14 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline; namespace osu.Game.Screens.Edit { + [Cached] public abstract partial class EditorScreenWithTimeline : EditorScreen { public const float PADDING = 10; - private Container timelineContainer = null!; + public Container TimelineContent = null!; - private Container mainContent = null!; + public Container MainContent = null!; private LoadingSpinner spinner = null!; @@ -70,7 +71,7 @@ namespace osu.Game.Screens.Edit { new Drawable[] { - timelineContainer = new Container + TimelineContent = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -93,7 +94,7 @@ namespace osu.Game.Screens.Edit }, new Drawable[] { - mainContent = new Container + MainContent = new Container { Name = "Main content", RelativeSizeAxes = Axes.Both, @@ -116,10 +117,10 @@ namespace osu.Game.Screens.Edit { spinner.State.Value = Visibility.Hidden; - mainContent.Add(content); + MainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(new TimelineArea(CreateTimelineContent()), timelineContainer.Add); + LoadComponentAsync(new TimelineArea(CreateTimelineContent()), TimelineContent.Add); }); } From 63e6eaf53880e326d43a75efec66134bb36bfe30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Oct 2023 17:55:21 +0900 Subject: [PATCH 433/896] Fix failing tests --- osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs | 2 +- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 0051488029..d8219ff36e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("click away", () => { - InputManager.MoveMouseTo(Editor.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One); + InputManager.MoveMouseTo(Editor.ChildrenOfType().First().ScreenSpaceDrawQuad.TopLeft + new Vector2(5)); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index ba570a9251..c7c7c4aa83 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -40,14 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components public PlacementBlueprint CurrentPlacement { get; private set; } - [Resolved] + [Resolved(canBeNull: true)] private EditorScreenWithTimeline editorScreen { get; set; } /// /// Positional input must be received outside the container's bounds, /// in order to handle composer blueprints which are partially offscreen. /// - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen.MainContent.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); public ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) From 0ed5f274f6e45ea22401a50e905e2a1d3406f170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 10:48:31 +0100 Subject: [PATCH 434/896] Enable NRT in `TestSceneSliderVelocityAdjust` --- .../Editor/TestSceneSliderVelocityAdjust.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index bb8c52bdfc..d92c6ebb60 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.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.Diagnostics; using System.Linq; using NUnit.Framework; @@ -24,15 +22,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public partial class TestSceneSliderVelocityAdjust : OsuGameTestScene { - private Screens.Edit.Editor editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor; + private Screens.Edit.Editor? editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor; - private EditorBeatmap editorBeatmap => editor.ChildrenOfType().FirstOrDefault(); + private EditorBeatmap editorBeatmap => editor.ChildrenOfType().FirstOrDefault()!; - private EditorClock editorClock => editor.ChildrenOfType().FirstOrDefault(); + private EditorClock editorClock => editor.ChildrenOfType().FirstOrDefault()!; - private Slider slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); + private Slider? slider => editorBeatmap.HitObjects.OfType().FirstOrDefault(); - private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault(); + private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType().FirstOrDefault()!; private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType().First(); @@ -66,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("ensure one slider placed", () => slider != null); - AddStep("store velocity", () => velocity = slider.Velocity); + AddStep("store velocity", () => velocity = slider!.Velocity); if (adjustVelocity) { @@ -76,10 +74,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("velocity adjusted", () => { Debug.Assert(velocity != null); - return Precision.AlmostEquals(velocity.Value * 2, slider.Velocity); + return Precision.AlmostEquals(velocity.Value * 2, slider!.Velocity); }); - AddStep("store velocity", () => velocity = slider.Velocity); + AddStep("store velocity", () => velocity = slider!.Velocity); } AddStep("save", () => InputManager.Keys(PlatformAction.Save)); @@ -88,8 +86,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); - AddStep("seek to slider", () => editorClock.Seek(slider.StartTime)); - AddAssert("slider has correct velocity", () => slider.Velocity == velocity); + AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime)); + AddAssert("slider has correct velocity", () => slider!.Velocity == velocity); } } } From e1ff0d12c66db4c2f16bc5d88fdb568dd5341bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 10:55:26 +0100 Subject: [PATCH 435/896] Update tests to NUnit-style assertions --- .../Editor/TestSceneSliderVelocityAdjust.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index d92c6ebb60..979801bc41 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.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.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Input; @@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor double? velocity = null; AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); - AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True); AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); @@ -58,11 +57,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("exit placement mode", () => InputManager.Key(Key.Number1)); - AddAssert("slider placed", () => slider != null); + AddAssert("slider placed", () => slider, () => Is.Not.Null); AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider)); - AddAssert("ensure one slider placed", () => slider != null); + AddAssert("ensure one slider placed", () => slider, () => Is.Not.Null); AddStep("store velocity", () => velocity = slider!.Velocity); @@ -71,11 +70,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); AddStep("change velocity", () => velocityTextBox.Current.Value = 2); - AddAssert("velocity adjusted", () => - { - Debug.Assert(velocity != null); - return Precision.AlmostEquals(velocity.Value * 2, slider!.Velocity); - }); + AddAssert("velocity adjusted", () => slider!.Velocity, + () => Is.EqualTo(velocity!.Value * 2).Within(Precision.DOUBLE_EPSILON)); AddStep("store velocity", () => velocity = slider!.Velocity); } @@ -84,10 +80,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("exit", () => InputManager.Key(Key.Escape)); AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader())); - AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True); AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime)); - AddAssert("slider has correct velocity", () => slider!.Velocity == velocity); + AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocity)); } } } From b3369dbb7b188d99f226894924a70339478c21b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 10:57:48 +0100 Subject: [PATCH 436/896] Add failing test for slider velocity --- .../Editor/TestSceneSliderVelocityAdjust.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs index 979801bc41..175cbeca6e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderVelocityAdjust.cs @@ -85,5 +85,50 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime)); AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocity)); } + + [Test] + public void TestVelocityUndo() + { + double? velocityBefore = null; + double? durationBefore = null; + + AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader())); + AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true); + + AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time)); + AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); + + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre)); + AddStep("start placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); + AddStep("end placement", () => InputManager.Click(MouseButton.Right)); + + AddStep("exit placement mode", () => InputManager.Key(Key.Number1)); + + AddAssert("slider placed", () => slider, () => Is.Not.Null); + AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider)); + + AddStep("store velocity", () => + { + velocityBefore = slider!.Velocity; + durationBefore = slider.Duration; + }); + + AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick()); + AddStep("change velocity", () => velocityTextBox.Current.Value = 2); + + AddAssert("velocity adjusted", () => slider!.Velocity, () => Is.EqualTo(velocityBefore!.Value * 2).Within(Precision.DOUBLE_EPSILON)); + + AddStep("undo", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.Z); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore)); + AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore)); + } } } From de89b7e53c305c0bf048b3ee42c028775d2da2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 10:59:02 +0100 Subject: [PATCH 437/896] Fix slider velocity changes not being undone correctly Closes https://github.com/ppy/osu/issues/25239. `LegacyEditorBeatmapPatcher.processHitObjectLocalData()` was already supposed to be handling changes to hitobjects that will show up neither when comparing the hitobjects themselves or the timing point with "legacy" info stripped - so, in other words, changes to slider velocity and samples. However, a change to slider velocity requires default application to take effect, so just resetting the value would visually fix the timeline marker but not change the actual object. Calling `EditorBeatmap.Update()` fixes this by way of triggering default re-application. This could probably be smarter (by only invoking the update when strictly necessary, etc.) - but I'm not sure it's worth the hassle. This is intended to be a quick fix, rather than a complete solution - the complete solution would indeed likely entail a wholesale restructuring of the editor's change handling. --- osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index fe0d4a7822..bb9f702cb5 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -123,6 +123,8 @@ namespace osu.Game.Screens.Edit oldWithRepeats.NodeSamples.Clear(); oldWithRepeats.NodeSamples.AddRange(newWithRepeats.NodeSamples); } + + editorBeatmap.Update(oldObject); } } From cea24298cb99c901077c6e8a23bdfa34da3ceafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 12:42:34 +0100 Subject: [PATCH 438/896] Privatise setters --- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index e1ec1ad4ac..575a66d421 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -16,9 +16,9 @@ namespace osu.Game.Screens.Edit { public const float PADDING = 10; - public Container TimelineContent = null!; + public Container TimelineContent { get; private set; } = null!; - public Container MainContent = null!; + public Container MainContent { get; private set; } = null!; private LoadingSpinner spinner = null!; From 88e10dd051d33ca0e63a7cd5f82dac3d9b4e039c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 20:03:44 +0100 Subject: [PATCH 439/896] 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 0575817460..2870696c03 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 9b06b4a6a7..f1159f58b9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 06508d08fe1f289404bb20756f43403c209e0d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 20:22:41 +0100 Subject: [PATCH 440/896] Delete outdated test --- .../UserInterface/TestSceneSearchTextBox.cs | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs deleted file mode 100644 index 153525d24a..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneSearchTextBox.cs +++ /dev/null @@ -1,38 +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 NUnit.Framework; -using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; -using osuTK; - -namespace osu.Game.Tests.Visual.UserInterface -{ - public partial class TestSceneSearchTextBox : OsuTestScene - { - private SearchTextBox textBox = null!; - - [SetUp] - public void SetUp() => Schedule(() => - { - Child = textBox = new SearchTextBox - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 400, - Scale = new Vector2(2f), - HoldFocus = true, - }; - }); - - [Test] - public void TestSelectionOnFocus() - { - AddStep("set text", () => textBox.Text = "some text"); - AddAssert("no text selected", () => textBox.SelectedText == string.Empty); - AddStep("hide text box", () => textBox.Hide()); - AddStep("show text box", () => textBox.Show()); - AddAssert("search text selected", () => textBox.SelectedText == textBox.Text); - } - } -} From f2c0bc821802067e8cb9c7e5bbf64215e6e4fc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 21:15:04 +0100 Subject: [PATCH 441/896] Add failing test case --- .../TestSceneSpinnerInput.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs index 5a473409a4..a6c15d5a67 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.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 NUnit.Framework; @@ -10,6 +11,8 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; @@ -47,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void Setup() => Schedule(() => { manualClock = null; + SelectedMods.Value = Array.Empty(); }); /// @@ -102,6 +106,34 @@ namespace osu.Game.Rulesets.Osu.Tests assertSpinnerHit(false); } + [Test] + public void TestVibrateWithoutSpinningOnCentreWithDoubleTime() + { + List frames = new List(); + + const int rate = 2; + // the track clock is going to be playing twice as fast, + // so the vibration time in clock time needs to be twice as long + // to keep constant speed in real time. + const int vibrate_time = 50 * rate; + + int direction = -1; + + for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time) + { + frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton)); + frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton)); + + direction *= -1; + } + + AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = rate } } }); + performTest(frames); + + assertTicksHit(0); + assertSpinnerHit(false); + } + /// /// Spins in a single direction. /// From e5b51f769ce72e7394131df3b993a7a840115984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 21:15:40 +0100 Subject: [PATCH 442/896] Fix incorrect assertion placement in spinner rotation tracker Checking the delta after the application of rate is not correct. The delta is in screen-space *before* the rate from rate-changing mods were applied; the point of the application of the rate is to compensate for the fact that the spinner is still judged in "track time" - but the goal is to keep the spinner's difficulty *independent* of rate, which means that with DT active the user's spin is "twice as effective" to compensate for the fact that the spinner is twice as short in real time. In another formulation, with DT active, the user gets to record replay frames "half as often" as in normal gameplay. --- .../Skinning/Default/SpinnerRotationTracker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index 374f3f461b..1d75663fd9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -101,11 +101,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default rotationTransferred = true; } + Debug.Assert(Math.Abs(delta) <= 180); + double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate; delta = (float)(delta * Math.Abs(rate)); - Debug.Assert(Math.Abs(delta) <= 180); - currentRotation += delta; drawableSpinner.Result.History.ReportDelta(Time.Current, delta); } From 12ef93ac3b42a86c031c37a9f89e430bf90912b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 21:31:34 +0100 Subject: [PATCH 443/896] Remove no-longer-valid assertion --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs index a6c15d5a67..75bcd809c8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerInput.cs @@ -130,7 +130,6 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = rate } } }); performTest(frames); - assertTicksHit(0); assertSpinnerHit(false); } From 87c9df937f85d3f7e9b220c2f21c13caae74b25b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 12:40:21 +0900 Subject: [PATCH 444/896] Move team seed to below team name --- .../Components/DrawableTeamSeed.cs | 6 +++++ .../TournamentSpriteTextWithBackground.cs | 2 +- .../Gameplay/Components/TeamDisplay.cs | 27 ++++++------------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamSeed.cs b/osu.Game.Tournament/Components/DrawableTeamSeed.cs index a79c63e979..077185f5c0 100644 --- a/osu.Game.Tournament/Components/DrawableTeamSeed.cs +++ b/osu.Game.Tournament/Components/DrawableTeamSeed.cs @@ -22,6 +22,12 @@ namespace osu.Game.Tournament.Components [Resolved] private LadderInfo ladder { get; set; } = null!; + [BackgroundDependencyLoader] + private void load() + { + Text.Font = Text.Font.With(size: 36); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs index ce118727cd..21439482e3 100644 --- a/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs +++ b/osu.Game.Tournament/Components/TournamentSpriteTextWithBackground.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tournament.Components { Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50), - Padding = new MarginPadding { Horizontal = 10 }, + Padding = new MarginPadding { Left = 10, Right = 20 }, Text = text, } }; diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 49fbc64397..3eec67c639 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -95,28 +95,17 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components } } }, - new FillFlowContainer + teamNameText = new TournamentSpriteTextWithBackground { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), + Scale = new Vector2(0.5f), + Origin = anchor, + Anchor = anchor, + }, + new DrawableTeamSeed(Team) + { + Scale = new Vector2(0.5f), Origin = anchor, Anchor = anchor, - Children = new Drawable[] - { - teamNameText = new TournamentSpriteTextWithBackground - { - Scale = new Vector2(0.5f), - Origin = anchor, - Anchor = anchor, - }, - new DrawableTeamSeed(Team) - { - Scale = new Vector2(0.5f), - Origin = anchor, - Anchor = anchor, - }, - } }, } }, From feeb95e4c389b85a1e72b1f00d2a79131ecb2652 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 12:44:43 +0900 Subject: [PATCH 445/896] Adjust `DrawableTeamTitleWithHeader` to match new layout --- .../Components/DrawableTeamTitleWithHeader.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs index fc4037d4e1..7d8fc847d4 100644 --- a/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs +++ b/osu.Game.Tournament/Components/DrawableTeamTitleWithHeader.cs @@ -18,21 +18,12 @@ namespace osu.Game.Tournament.Components { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), + Spacing = new Vector2(0, 5), Children = new Drawable[] { new DrawableTeamHeader(colour), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new DrawableTeamTitle(team), - new DrawableTeamSeed(team), - } - } + new DrawableTeamTitle(team), + new DrawableTeamSeed(team), } }; } From a3dc9a73b19120c0c4ef4ba7199463fb63ad280c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 13:35:15 +0900 Subject: [PATCH 446/896] Revert behaviour changes of `MaxDimensions` test and ignore instead --- .../Gameplay/TestScenePlayerMaxDimensions.cs | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs index 6665295e99..53a4abdd07 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerMaxDimensions.cs @@ -3,14 +3,21 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Skinning; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; namespace osu.Game.Tests.Visual.Gameplay { @@ -21,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay /// /// The HUD is hidden as it does't really affect game balance if HUD elements are larger than they should be. /// + [Ignore("This test is for visual testing, and has no value in being run in standard CI runs.")] public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers { // scale textures to 4 times their size. @@ -66,18 +74,66 @@ namespace osu.Game.Tests.Visual.Gameplay remove { } } - public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) - { - var texture = base.GetTexture(componentName, wrapModeS, wrapModeT); - - if (texture != null) - texture.ScaleAdjust /= scale_factor; - - return texture; - } - public ISkin FindProvider(Func lookupFunction) => this; public IEnumerable AllSources => new[] { this }; + + protected override IResourceStore CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore storage) + => new UpscaledTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage)); + + private class UpscaledTextureLoaderStore : IResourceStore + { + private readonly IResourceStore? textureStore; + + public UpscaledTextureLoaderStore(IResourceStore? textureStore) + { + this.textureStore = textureStore; + } + + public void Dispose() + { + textureStore?.Dispose(); + } + + public TextureUpload Get(string name) + { + var textureUpload = textureStore?.Get(name); + + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureUpload == null) + return null!; + + return upscale(textureUpload); + } + + public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) + { + // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp. + if (textureStore == null) + return null!; + + var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false); + + if (textureUpload == null) + return null!; + + return await Task.Run(() => upscale(textureUpload), cancellationToken).ConfigureAwait(false); + } + + private TextureUpload upscale(TextureUpload textureUpload) + { + var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height); + + // The original texture upload will no longer be returned or used. + textureUpload.Dispose(); + + image.Mutate(i => i.Resize(new Size(textureUpload.Width, textureUpload.Height) * scale_factor)); + return new TextureUpload(image); + } + + public Stream? GetStream(string name) => textureStore?.GetStream(name); + + public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty(); + } } } } From 37ec10d4f592ebab514c9a9afc5f9dcb8dce7c2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 13:49:39 +0900 Subject: [PATCH 447/896] Fix `TestSongSelectScrollHandling` not waiting for `VolumeOverlay` to load See https://github.com/ppy/osu/actions/runs/6701786492/job/18210372721. --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 7fa4f8c836..9e743ef336 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Navigation double scrollPosition = 0; AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); - AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().Single().State.Value, () => Is.EqualTo(Visibility.Hidden)); + AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); From 89444d5544aad8c89be4eff311cd6beeedc1c421 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:00:49 +0900 Subject: [PATCH 448/896] Fix export test still occasionally failing due to file write in progress https://github.com/ppy/osu/actions/runs/6701591401/job/18209826074 Basically, `File.Move` may not be an atomic operation. --- .../Gameplay/TestScenePlayerLocalScoreImport.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs index 1254aa0639..0dd544bb30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs @@ -214,10 +214,18 @@ namespace osu.Game.Tests.Visual.Gameplay // Files starting with _ are temporary, created by CreateFileSafely call. AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null); - AddAssert("filesize is non-zero", () => + AddUntilStep("filesize is non-zero", () => { - using (var stream = LocalStorage.GetStream(filePath)) - return stream.Length; + try + { + using (var stream = LocalStorage.GetStream(filePath)) + return stream.Length; + } + catch (IOException) + { + // file move may still be in progress. + return 0; + } }, () => Is.Not.Zero); } From 66b84d02cb1631302631d5b16afd31d0d420a13a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:20:11 +0900 Subject: [PATCH 449/896] Add note about `TestGameplayExitFlow` failure, and ignore for now See: https://github.com/ppy/osu/actions/runs/6695995685/job/18194110641 https://github.com/ppy/osu/actions/runs/6700910613/job/18208272419 --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 09624f63b7..16030d568b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -693,7 +693,9 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - [FlakyTest] // See above + [Ignore("Failing too often, needs revisiting in some future.")] + // This test is failing even after 10 retries (see https://github.com/ppy/osu/actions/runs/6700910613/job/18208272419) + // Something is stopping the ready button from changing states, over multiple runs. public void TestGameplayExitFlow() { Bindable? holdDelay = null; From bdd3f2847b8a27d1e34657297fdc290deeaf1e88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:26:00 +0900 Subject: [PATCH 450/896] Add an extra storyboard sample to avoid intermittent failures in `TestStoryboardSamplesStopOnSkip` Probably CI running slow timing balls. The point of failure is `waitUntilStoryboardSamplesPlay()` after already testing the important part of the test (that the samples stop on skip) so let's give it another possible point to recover. See https://github.com/ppy/osu/actions/runs/6698399814/job/18201753701. --- .../Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index a9d4508f70..11dc0f9c30 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20)); backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20)); backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20)); + backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 2000, volume: 20)); } [SetUp] From d379e553da920cfd8cbccca2f5a03786c8627dc6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:31:26 +0900 Subject: [PATCH 451/896] Fix back-to-front logging --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index b072ce191e..29c9381ee4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Gameplay alwaysGoingForward &= goingForward; if (!goingForward) - Logger.Log($"Backwards time occurred ({currentTime:N1} -> {lastTime:N1})"); + Logger.Log($"Backwards time occurred ({lastTime:N1} -> {currentTime:N1})"); lastTime = currentTime; }; From 7ceced70122d51ffc030074d1f3738645367be59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:47:04 +0900 Subject: [PATCH 452/896] Scope `TestPauseWithLargeOffset` to focus on what matters See https://github.com/ppy/osu/actions/runs/6693917410/job/18186111009 This test is to make sure we don't seek before the original pause time, so I've exposed that value precisely to avoid CI woes. --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 16 ++++++++++------ .../Screens/Play/MasterGameplayClockContainer.cs | 12 ++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 29c9381ee4..ec3b3e0822 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseWithLargeOffset() { - double lastTime; + double lastStopTime; bool alwaysGoingForward = true; AddStep("force large offset", () => @@ -84,20 +84,24 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add time forward check hook", () => { - lastTime = double.MinValue; + lastStopTime = double.MinValue; alwaysGoingForward = true; Player.OnUpdate += _ => { - double currentTime = Player.GameplayClockContainer.CurrentTime; - bool goingForward = currentTime >= lastTime - 500; + var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer; + + double currentTime = masterClock.CurrentTime; + + bool goingForward = currentTime >= (masterClock.LastStopTime ?? lastStopTime); alwaysGoingForward &= goingForward; if (!goingForward) - Logger.Log($"Backwards time occurred ({lastTime:N1} -> {currentTime:N1})"); + Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})"); - lastTime = currentTime; + if (masterClock.LastStopTime != null) + lastStopTime = masterClock.LastStopTime.Value; }; }); diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 1c860e9d4b..54ed7ba626 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play /// /// In the future I want to change this. /// - private double? actualStopTime; + internal double? LastStopTime; [Resolved] private MusicController musicController { get; set; } = null!; @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Play protected override void StopGameplayClock() { - actualStopTime = GameplayClock.CurrentTime; + LastStopTime = GameplayClock.CurrentTime; if (IsLoaded) { @@ -127,17 +127,17 @@ namespace osu.Game.Screens.Play public override void Seek(double time) { // Safety in case the clock is seeked while stopped. - actualStopTime = null; + LastStopTime = null; base.Seek(time); } protected override void PrepareStart() { - if (actualStopTime != null) + if (LastStopTime != null) { - Seek(actualStopTime.Value); - actualStopTime = null; + Seek(LastStopTime.Value); + LastStopTime = null; } else base.PrepareStart(); From 8c067dc584066527badb6a9029f5ef31a932bba8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 14:53:07 +0900 Subject: [PATCH 453/896] Fix mod tests not waiting for presets to finish loading See https://github.com/ppy/osu/actions/runs/6692350567/job/18181352850. --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 3728fb3f21..f0822ce2a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -799,8 +799,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType().Single().Current.Value, () => Is.EqualTo(0.7)); } - private void waitForColumnLoad() => AddUntilStep("all column content loaded", - () => modSelectOverlay.ChildrenOfType().Any() && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => + modSelectOverlay.ChildrenOfType().Any() + && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded) + && modSelectOverlay.ChildrenOfType().Any() + && modSelectOverlay.ChildrenOfType().All(column => column.IsLoaded)); private void changeRuleset(int id) { From 64efc3d251ef1c4ff39f2d3b2c25df4dc020ae6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 15:33:40 +0900 Subject: [PATCH 454/896] Decouple metronome tick playback from pendulum movement Not super happy about doing this, but it seems like it's in the best interest of editor usability. --- .../Screens/Edit/Timing/MetronomeDisplay.cs | 74 +++++++++++++------ 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 9f03281d0c..29e730c865 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -34,16 +34,18 @@ namespace osu.Game.Screens.Edit.Timing private IAdjustableClock metronomeClock = null!; - private Sample? sampleTick; - private Sample? sampleTickDownbeat; private Sample? sampleLatch; - private ScheduledDelegate? tickPlaybackDelegate; + private readonly MetronomeTick metronomeTick = new MetronomeTick(); [Resolved] private OverlayColourProvider overlayColourProvider { get; set; } = null!; - public bool EnableClicking { get; set; } = true; + public bool EnableClicking + { + get => metronomeTick.EnableClicking; + set => metronomeTick.EnableClicking = value; + } public MetronomeDisplay() { @@ -53,8 +55,6 @@ namespace osu.Game.Screens.Edit.Timing [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleTick = audio.Samples.Get(@"UI/metronome-tick"); - sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat"); sampleLatch = audio.Samples.Get(@"UI/metronome-latch"); const float taper = 25; @@ -67,8 +67,11 @@ namespace osu.Game.Screens.Edit.Timing AutoSizeAxes = Axes.Both; + metronomeTick.Ticked = onTickPlayed; + InternalChildren = new Drawable[] { + metronomeTick, new Container { Name = @"Taper adjust", @@ -265,9 +268,6 @@ namespace osu.Game.Screens.Edit.Timing isSwinging = false; - tickPlaybackDelegate?.Cancel(); - tickPlaybackDelegate = null; - // instantly latch if pendulum arm is close enough to center (to prevent awkward delayed playback of latch sound) if (Precision.AlmostEquals(swing.Rotation, 0, 1)) { @@ -306,27 +306,53 @@ namespace osu.Game.Screens.Edit.Timing float targetAngle = currentAngle > 0 ? -angle : angle; swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad); + } - if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging) + private void onTickPlayed() + { + // Originally, this flash only occurred when the pendulum correctly passess the centre. + // Mappers weren't happy with the metronome tick not playing immediately after starting playback + // so now this matches the actual tick sample. + stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); + } + + private partial class MetronomeTick : BeatSyncedContainer + { + public bool EnableClicking; + + private Sample? sampleTick; + private Sample? sampleTickDownbeat; + + public Action? Ticked; + + public MetronomeTick() { - using (BeginDelayedSequence(beatLength / 2)) - { - stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); + AllowMistimedEventFiring = false; + } - tickPlaybackDelegate = Schedule(() => - { - if (!EnableClicking) - return; + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleTick = audio.Samples.Get(@"UI/metronome-tick"); + sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat"); + } - var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel(); + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - if (channel == null) - return; + if (!IsBeatSyncedWithTrack || !EnableClicking) + return; - channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f); - channel.Play(); - }); - } + var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel(); + + if (channel == null) + return; + + channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f); + channel.Play(); + + Ticked?.Invoke(); } } } From 144006fbe83e60b6ba5a0cab7856eb3c4416da28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 08:38:22 +0100 Subject: [PATCH 455/896] Update autoselect implementation to work correctly with framework changes --- .../Edit/Compose/Components/BeatDivisorControl.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 7eba1fe1cd..b33edb9edb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -321,7 +321,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Spacing = new Vector2(10), Children = new Drawable[] { - divisorTextBox = new OsuNumberBox + divisorTextBox = new AutoSelectTextBox { RelativeSizeAxes = Axes.X, PlaceholderText = "Beat divisor" @@ -341,8 +341,6 @@ namespace osu.Game.Screens.Edit.Compose.Components base.LoadComplete(); BeatDivisor.BindValueChanged(_ => updateState(), true); divisorTextBox.OnCommit += (_, _) => setPresetsFromTextBoxEntry(); - - divisorTextBox.SelectAll(); } private void setPresetsFromTextBoxEntry() @@ -590,5 +588,16 @@ namespace osu.Game.Screens.Edit.Compose.Components } } } + + private partial class AutoSelectTextBox : OsuNumberBox + { + protected override void LoadComplete() + { + base.LoadComplete(); + + GetContainingInputManager().ChangeFocus(this); + SelectAll(); + } + } } } From 0d44b5af90d606657e63c9c386574dcd5e18c89f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 31 Oct 2023 17:36:23 +0900 Subject: [PATCH 456/896] Fix potential texture corruption when cropping gameplay textures of weird aspet ratios Closes https://github.com/ppy/osu/issues/25273. --- osu.Game/Skinning/LegacySkinExtensions.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 62197fa8a7..a8ec67d98b 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -115,7 +115,18 @@ namespace osu.Game.Skinning maxSize *= texture.ScaleAdjust; - var croppedTexture = texture.Crop(new RectangleF(texture.Width / 2f - maxSize.X / 2f, texture.Height / 2f - maxSize.Y / 2f, maxSize.X, maxSize.Y)); + // Importantly, check per-axis for the minimum dimension to avoid accidentally inflating + // textures with weird aspect ratios. + float newWidth = Math.Min(texture.Width, maxSize.X); + float newHeight = Math.Min(texture.Height, maxSize.Y); + + var croppedTexture = texture.Crop(new RectangleF( + texture.Width / 2f - newWidth / 2f, + texture.Height / 2f - newHeight / 2f, + newWidth, + newHeight + )); + croppedTexture.ScaleAdjust = texture.ScaleAdjust; return croppedTexture; } From 9f5a280bc22b1f88d20b7b885d4aa252f7feee7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 12:25:08 +0100 Subject: [PATCH 457/896] Fix key binding row fire-and-forgetting writes Intends to fix test failures as seen in https://github.com/ppy/osu/actions/runs/6692350567/job/18181352642#step:5:129 This is what happens if you carelessly fire and forget. The working theory (that I'm not sure I have the tools to conclusively confirm) is that the async write from the key binding changing could fire _after_ the section is reset. I briefly considered having the test wait for the change, but given that the entirety of the surrounding flow is using sync operations, this just looks like a bug to me. And there's no real sane way to inject async into that flow due to dependence on `OsuButton.Action`. --- osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index c85fe4727a..e82cebe9f4 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -498,7 +498,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (existingBinding == null) { - realm.WriteAsync(r => r.Find(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString()); + realm.Write(r => r.Find(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString()); BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: false, advanceToNextBinding)); return; } From 7ea298a1b6a4f17a8ae3b505c599f04ac498e4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 16:13:44 +0100 Subject: [PATCH 458/896] Add xmldoc to `Mod` playability flags --- osu.Game/Rulesets/Mods/Mod.cs | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index a0bdc9ff51..49c2fdd394 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -107,12 +107,52 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool HasImplementation => this is IApplicableMod; + /// + /// Whether this mod can be played by a real human user. + /// Non-user-playable mods are not viable for single-player score submission. + /// + /// + /// + /// is user-playable. + /// is not user-playable. + /// + /// [JsonIgnore] public virtual bool UserPlayable => true; + /// + /// Whether this mod can be specified as a "required" mod in a multiplayer context. + /// + /// + /// + /// is valid for multiplayer. + /// + /// is valid for multiplayer as long as it is a required mod, + /// as that ensures the same duration of gameplay for all users in the room. + /// + /// + /// is not valid for multiplayer, as it leads to varying + /// gameplay duration depending on how the users in the room play. + /// + /// is not valid for multiplayer. + /// + /// [JsonIgnore] public virtual bool ValidForMultiplayer => true; + /// + /// Whether this mod can be specified as a "free" or "allowed" mod in a multiplayer context. + /// + /// + /// + /// is valid for multiplayer as a free mod. + /// + /// is not valid for multiplayer as a free mod, + /// as it could to varying gameplay duration between users in the room depending on whether they picked it. + /// + /// is not valid for multiplayer as a free mod. + /// + /// [JsonIgnore] public virtual bool ValidForMultiplayerAsFreeMod => true; From 3a2645efb17dbe9bee7251b4a8ecc0868cf8799d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 16:15:10 +0100 Subject: [PATCH 459/896] Seal `ModAutoplay` playability flags Can't do much more than that due to the unfortunate fact of Cinema inheriting from Autoplay. --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index a3a4adc53d..973fcffba8 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -20,9 +20,9 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; - public override bool UserPlayable => false; - public override bool ValidForMultiplayer => false; - public override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool UserPlayable => false; + public sealed override bool ValidForMultiplayer => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) }; From 456f4ebba2208d4a764df157d02dcbbbe6dda544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 16:16:14 +0100 Subject: [PATCH 460/896] Seal `ModScoreV2` Nobody should ever need to extend it. --- osu.Game/Rulesets/Mods/ModScoreV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs index df83d96769..52dcd71666 100644 --- a/osu.Game/Rulesets/Mods/ModScoreV2.cs +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods /// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active. /// It should not be used in any real capacity going forward. /// - public class ModScoreV2 : Mod + public sealed class ModScoreV2 : Mod { public override string Name => "Score V2"; public override string Acronym => @"SV2"; From a644c75957247c8f16674ea47e1eec4a239f1159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 16:16:57 +0100 Subject: [PATCH 461/896] Mark `ModScoreV2` as invalid for multiplayer Doesn't do much for the client; mostly a safety for osu-web's sake, as without the change it could theoretically fail to validate the mod properly in multiplayer contexts. --- osu.Game/Rulesets/Mods/ModScoreV2.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs index 52dcd71666..6a77cafa30 100644 --- a/osu.Game/Rulesets/Mods/ModScoreV2.cs +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -17,5 +17,7 @@ namespace osu.Game.Rulesets.Mods public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active."; public override double ScoreMultiplier => 1; public override bool UserPlayable => false; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; } } From 955e2ed05166aeaf6073938661a9bc7197d9b816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 16:18:09 +0100 Subject: [PATCH 462/896] Seal `UnknownMod` Not a class for extension. --- osu.Game/Rulesets/Mods/UnknownMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index abe05996ff..31fc09b0a6 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -5,7 +5,7 @@ using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { - public class UnknownMod : Mod + public sealed class UnknownMod : Mod { /// /// The acronym of the mod which could not be resolved. From a90f8dd4f63191209d593201a2bb1a00b7828a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 16:20:33 +0100 Subject: [PATCH 463/896] Seal a few more multiplayer playability flags of rate-changing mods Not really changing anything, just tightening things down to curb possible funny business. --- osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs | 4 ++-- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 607e6b8399..77aa5cdc15 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 0.5; - public override bool ValidForMultiplayer => false; - public override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool ValidForMultiplayer => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) }; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index fa1c143585..e5af758b4f 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { - public override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; public abstract BindableNumber SpeedChange { get; } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index d2772417db..36e4522771 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } - public override bool ValidForMultiplayerAsFreeMod => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; From c2de03aa44c85fccb29e5542c63c01d46dc7145d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Nov 2023 18:28:05 +0900 Subject: [PATCH 464/896] Fix all spinner ticks being alive and causing performance degradation Regressed in https://github.com/ppy/osu/pull/25216. The new logic will ensure at least one tick is ready for judgement. There shouldn't be a case where more than one is needed in a single frame. --- .../Objects/Drawables/DrawableSpinner.cs | 13 +++++++++++++ .../Objects/Drawables/DrawableSpinnerTick.cs | 8 ++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index c092b4dd4b..2799ba4a25 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -275,6 +275,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (spinningSample != null && spinnerFrequencyModulate) spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress; + + // Ticks can theoretically be judged at any point in the spinner's duration. + // For performance reasons, we only want to keep the next tick alive. + var next = NestedHitObjects.FirstOrDefault(h => !h.Judged); + + // See default `LifetimeStart` as set in `DrawableSpinnerTick`. + if (next?.LifetimeStart == double.MaxValue) + { + // the tick can be theoretically judged at any point in the spinner's duration, + // so it must be alive throughout the spinner's entire lifetime. + // this mostly matters for correct sample playback. + next.LifetimeStart = HitObject.StartTime; + } } protected override void UpdateAfterChildren() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index a5785dd1f6..5b55533edd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public override bool DisplayResult => false; - protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject; - public DrawableSpinnerTick() : this(null) { @@ -29,10 +27,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.OnApply(); - // the tick can be theoretically judged at any point in the spinner's duration, - // so it must be alive throughout the spinner's entire lifetime. - // this mostly matters for correct sample playback. - LifetimeStart = DrawableSpinner.HitObject.StartTime; + // Lifetime will be managed by `DrawableSpinner`. + LifetimeStart = double.MaxValue; } /// From 48bdeaeff14f456abb371878307c91d79156832e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 02:42:36 +0900 Subject: [PATCH 465/896] Fix another potential crash in bubbles mod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Storing `DrawableHitObject` for later use is not safe – especially when accessing `HitObject` – as it's a pooled class. In the case here, it's important to note that `PrepareForUse` can be called a frame or more later in execution, which made this unsafe. Closes https://github.com/ppy/osu/issues/24444. --- osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs | 77 ++++++++++----------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs index 9c0e43e96f..b34cc29741 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs @@ -2,11 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; 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; @@ -22,6 +20,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -90,21 +89,18 @@ namespace osu.Game.Rulesets.Osu.Mods break; default: - addBubble(); + BubbleDrawable bubble = bubblePool.Get(); + + bubble.WasHit = drawable.IsHit; + bubble.Position = getPosition(drawableOsuHitObject); + bubble.AccentColour = drawable.AccentColour.Value; + bubble.InitialSize = new Vector2(bubbleSize); + bubble.FadeTime = bubbleFade; + bubble.MaxSize = maxSize; + + bubbleContainer.Add(bubble); break; } - - void addBubble() - { - BubbleDrawable bubble = bubblePool.Get(); - - bubble.DrawableOsuHitObject = drawableOsuHitObject; - bubble.InitialSize = new Vector2(bubbleSize); - bubble.FadeTime = bubbleFade; - bubble.MaxSize = maxSize; - - bubbleContainer.Add(bubble); - } }; drawableObject.OnRevertResult += (drawable, _) => @@ -118,18 +114,38 @@ namespace osu.Game.Rulesets.Osu.Mods }; } + private Vector2 getPosition(DrawableOsuHitObject drawableObject) + { + switch (drawableObject) + { + // SliderHeads are derived from HitCircles, + // so we must handle them before to avoid them using the wrong positioning logic + case DrawableSliderHead: + return drawableObject.HitObject.Position; + + // Using hitobject position will cause issues with HitCircle placement due to stack leniency. + case DrawableHitCircle: + return drawableObject.Position; + + default: + return drawableObject.HitObject.Position; + } + } + #region Pooled Bubble drawable private partial class BubbleDrawable : PoolableDrawable { - public DrawableOsuHitObject? DrawableOsuHitObject { get; set; } - public Vector2 InitialSize { get; set; } public float MaxSize { get; set; } public double FadeTime { get; set; } + public bool WasHit { get; set; } + + public Color4 AccentColour { get; set; } + private readonly Box colourBox; private readonly CircularContainer content; @@ -157,15 +173,12 @@ namespace osu.Game.Rulesets.Osu.Mods protected override void PrepareForUse() { - Debug.Assert(DrawableOsuHitObject.IsNotNull()); - - Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black; + Colour = WasHit ? Colour4.White : Colour4.Black; Scale = new Vector2(1); - Position = getPosition(DrawableOsuHitObject); Size = InitialSize; //We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect. - ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f); + ColourInfo colourDarker = AccentColour.Darken(0.1f); // The absolute length of the bubble's animation, can be used in fractions for animations of partial length double duration = 1700 + Math.Pow(FadeTime, 1.07f); @@ -178,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Mods .ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint) .FadeOut(duration * 0.2f, Easing.OutCirc).Expire(); - if (!DrawableOsuHitObject.IsHit) return; + if (!WasHit) return; content.BorderThickness = InitialSize.X / 3.5f; content.BorderColour = Colour4.White; @@ -192,24 +205,6 @@ namespace osu.Game.Rulesets.Osu.Mods // Avoids transparency overlap issues during the bubble "pop" .TransformTo(nameof(BorderThickness), 0f); } - - private Vector2 getPosition(DrawableOsuHitObject drawableObject) - { - switch (drawableObject) - { - // SliderHeads are derived from HitCircles, - // so we must handle them before to avoid them using the wrong positioning logic - case DrawableSliderHead: - return drawableObject.HitObject.Position; - - // Using hitobject position will cause issues with HitCircle placement due to stack leniency. - case DrawableHitCircle: - return drawableObject.Position; - - default: - return drawableObject.HitObject.Position; - } - } } #endregion From 6dab5ee4cfbad32710777b649d5fd5e6cc1d2c3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 15:13:43 +0900 Subject: [PATCH 466/896] Add support for "argon" default skin to expand columns when on mobile device Should ease those looking to play the game on mobile until we (potentially) have a better solution in the future. If this works out well, we can consider rolling it out to other skins. Closes https://github.com/ppy/osu/issues/23377. --- .../Skinning/Argon/ManiaArgonSkinTransformer.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index ddd6365c25..f74b7f1d02 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -99,9 +100,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon return SkinUtils.As(new Bindable(30)); case LegacyManiaSkinConfigurationLookups.ColumnWidth: - return SkinUtils.As(new Bindable( - stage.IsSpecialColumn(columnIndex) ? 120 : 60 - )); + + float width = 60; + + // Best effort until we have better mobile support. + if (RuntimeInfo.IsMobile) + width = 180 * Math.Min(1, 7f / beatmap.TotalColumns); + + return SkinUtils.As(new Bindable(stage.IsSpecialColumn(columnIndex) ? width * 2 : width)); case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: From c83589cb742db95d7fb21df6aa2c00836a01be89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 15:49:10 +0900 Subject: [PATCH 467/896] Change osu!mania conversion mod ordering to be more appeasing --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 0055e10ada..0c317e0f8a 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -255,16 +255,6 @@ namespace osu.Game.Rulesets.Mania case ModType.Conversion: return new Mod[] { - new MultiMod(new ManiaModKey4(), - new ManiaModKey5(), - new ManiaModKey6(), - new ManiaModKey7(), - new ManiaModKey8(), - new ManiaModKey9(), - new ManiaModKey10(), - new ManiaModKey1(), - new ManiaModKey2(), - new ManiaModKey3()), new ManiaModRandom(), new ManiaModDualStages(), new ManiaModMirror(), @@ -272,7 +262,19 @@ namespace osu.Game.Rulesets.Mania new ManiaModClassic(), new ManiaModInvert(), new ManiaModConstantSpeed(), - new ManiaModHoldOff() + new ManiaModHoldOff(), + new MultiMod( + new ManiaModKey1(), + new ManiaModKey2(), + new ManiaModKey3(), + new ManiaModKey4(), + new ManiaModKey5(), + new ManiaModKey6(), + new ManiaModKey7(), + new ManiaModKey8(), + new ManiaModKey9(), + new ManiaModKey10() + ), }; case ModType.Automation: From ad82ada030715427397d6ef5b0a08b475dbfef31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 08:18:37 +0100 Subject: [PATCH 468/896] Trim redundant comments --- .../Objects/Drawables/DrawableSpinner.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2799ba4a25..2e9a4d92ec 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -277,17 +277,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress; // Ticks can theoretically be judged at any point in the spinner's duration. - // For performance reasons, we only want to keep the next tick alive. + // A tick must be alive to correctly play back samples, + // but for performance reasons, we only want to keep the next tick alive. var next = NestedHitObjects.FirstOrDefault(h => !h.Judged); // See default `LifetimeStart` as set in `DrawableSpinnerTick`. if (next?.LifetimeStart == double.MaxValue) - { - // the tick can be theoretically judged at any point in the spinner's duration, - // so it must be alive throughout the spinner's entire lifetime. - // this mostly matters for correct sample playback. next.LifetimeStart = HitObject.StartTime; - } } protected override void UpdateAfterChildren() From 9c1f4b552e651a8d163cd7adf17a0c6ff3e49f08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 17:04:49 +0900 Subject: [PATCH 469/896] Rename and invert flags for slider classic behaviours --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 4 +- .../Objects/Drawables/DrawableSlider.cs | 47 ++++++++++--------- .../Objects/Drawables/DrawableSliderHead.cs | 13 ++++- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 +++- .../Objects/SliderHeadCircle.cs | 4 +- 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 8930b4ad70..c668119db7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -41,10 +41,10 @@ namespace osu.Game.Rulesets.Osu.Mods switch (hitObject) { case Slider slider: - slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value; + slider.ClassicSliderBehaviour = NoSliderHeadAccuracy.Value; foreach (var head in slider.NestedHitObjects.OfType()) - head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value; + head.ClassicSliderBehaviour = NoSliderHeadAccuracy.Value; break; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index cdfe888c99..a053c99a53 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public Container OverlayElementContainer { get; private set; } - public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects; + public override bool DisplayResult => HitObject.ClassicSliderBehaviour; [CanBeNull] public PlaySliderBody SliderBody => Body.Drawable as PlaySliderBody; @@ -272,30 +272,31 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || !TailCircle.Judged || Time.Current < HitObject.EndTime) return; - // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. - // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). - if (HitObject.OnlyJudgeNestedObjects) + if (HitObject.ClassicSliderBehaviour) { - ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); - return; - } - - // Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. - ApplyResult(r => - { - int totalTicks = NestedHitObjects.Count; - int hitTicks = NestedHitObjects.Count(h => h.IsHit); - - if (hitTicks == totalTicks) - r.Type = HitResult.Great; - else if (hitTicks == 0) - r.Type = HitResult.Miss; - else + // Classic behaviour means a slider is judged proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. + ApplyResult(r => { - double hitFraction = (double)hitTicks / totalTicks; - r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; - } - }); + int totalTicks = NestedHitObjects.Count; + int hitTicks = NestedHitObjects.Count(h => h.IsHit); + + if (hitTicks == totalTicks) + r.Type = HitResult.Great; + else if (hitTicks == 0) + r.Type = HitResult.Miss; + else + { + double hitFraction = (double)hitTicks / totalTicks; + r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; + } + }); + } + else + { + // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. + // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). + ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); + } } public override void PlaySamples() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index be6c322668..d99ea70fbd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; - public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult; + public override bool DisplayResult + { + get + { + if (HitObject?.ClassicSliderBehaviour == true) + return false; + + return base.DisplayResult; + } + } private readonly IBindable pathVersion = new Bindable(); @@ -56,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Debug.Assert(HitObject != null); - if (HitObject.JudgeAsNormalHitCircle) + if (!HitObject.ClassicSliderBehaviour) return base.ResultFor(timeOffset); // If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring. diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 3cb9b96090..bcbed8b17f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.Objects /// Whether this 's judgement is fully handled by its nested s. /// If false, this will be judged proportionally to the number of nested s hit. /// - public bool OnlyJudgeNestedObjects = true; + public bool ClassicSliderBehaviour = false; public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { @@ -262,7 +262,11 @@ namespace osu.Game.Rulesets.Osu.Objects TailSamples = this.GetNodeSamples(repeatCount + 1); } - public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement(); + public override Judgement CreateJudgement() => ClassicSliderBehaviour + // See logic in `DrawableSlider.CheckForResult()` + ? new OsuJudgement() + // Of note, this creates a combo discrepancy for non-classic-mod sliders (there is no combo increase for tail or slider judgement). + : new OsuIgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 73c222653e..4712d61dfe 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Osu.Objects /// Whether to treat this as a normal for judgement purposes. /// If false, this will be judged as a instead. /// - public bool JudgeAsNormalHitCircle = true; + public bool ClassicSliderBehaviour; - public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement(); + public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new SliderTickJudgement() : base.CreateJudgement(); } } From 9af2a5930cf3696ffecea558e94bcec01b642ace Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 17:39:41 +0900 Subject: [PATCH 470/896] Remove redundant passing of `Scale` to nested objects --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index bcbed8b17f..0859a6fe17 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -187,7 +187,6 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = e.Time, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, - Scale = Scale, }); break; @@ -206,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Objects RepeatIndex = e.SpanIndex, StartTime = e.Time, Position = EndPosition, - StackHeight = StackHeight + StackHeight = StackHeight, }); break; @@ -217,7 +216,6 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, - Scale = Scale, }); break; } From a7705284e7b56bcedaee756ec8f37432f029d62c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 18:02:47 +0900 Subject: [PATCH 471/896] Add better commenting around `DrawableSliderHead` logic --- .../Objects/Drawables/DrawableSliderHead.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index d99ea70fbd..8463c78319 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -65,12 +65,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Debug.Assert(HitObject != null); - if (!HitObject.ClassicSliderBehaviour) - return base.ResultFor(timeOffset); - - // If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring. - var result = base.ResultFor(timeOffset); - return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; + return HitObject.ClassicSliderBehaviour + // In classic slider behaviour, heads are considered fully hit if in the largest hit window. + // We can't award a full Great because the true Great judgement is awarded on the Slider itself, + // reduced based on number of ticks hit. + // + // so we use the most suitable LargeTick judgement here instead. + ? base.ResultFor(timeOffset).IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss + : base.ResultFor(timeOffset); } public override void Shake() From bf9f20705f06e3094ae7e772f4ac59fa45472856 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 17:39:23 +0900 Subject: [PATCH 472/896] Simplify classic behaviour flag to only need to be specified on the slider itself --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 4 ---- osu.Game.Rulesets.Osu/Objects/Slider.cs | 14 +++++++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index c668119db7..f20f95b384 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -42,10 +42,6 @@ namespace osu.Game.Rulesets.Osu.Mods { case Slider slider: slider.ClassicSliderBehaviour = NoSliderHeadAccuracy.Value; - - foreach (var head in slider.NestedHitObjects.OfType()) - head.ClassicSliderBehaviour = NoSliderHeadAccuracy.Value; - break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 0859a6fe17..50791183f5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -127,7 +127,18 @@ namespace osu.Game.Rulesets.Osu.Objects /// Whether this 's judgement is fully handled by its nested s. /// If false, this will be judged proportionally to the number of nested s hit. /// - public bool ClassicSliderBehaviour = false; + public bool ClassicSliderBehaviour + { + get => classicSliderBehaviour; + set + { + classicSliderBehaviour = value; + if (HeadCircle != null) + HeadCircle.ClassicSliderBehaviour = value; + } + } + + private bool classicSliderBehaviour; public BindableNumber SliderVelocityMultiplierBindable { get; } = new BindableDouble(1) { @@ -196,6 +207,7 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = e.Time, Position = Position, StackHeight = StackHeight, + ClassicSliderBehaviour = ClassicSliderBehaviour, }); break; From 818432fab4279b6691286483b6be88eb663e84c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 19:27:55 +0900 Subject: [PATCH 473/896] Fix non-classic osu! combo not matching expectations --- osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs | 9 +++++++++ osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs | 11 ----------- .../Objects/SliderTailCircle.cs | 16 ++++++++++++++-- osu.Game/Rulesets/Scoring/HitResult.cs | 3 +++ 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index ddbbb300ca..88a34fcb8f 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -3,6 +3,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects @@ -43,5 +45,12 @@ namespace osu.Game.Rulesets.Osu.Objects } protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public override Judgement CreateJudgement() => new SliderEndJudgement(); + + public class SliderEndJudgement : OsuJudgement + { + public override HitResult MaxResult => HitResult.LargeTickHit; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index cca86361c2..e95cfd369d 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -1,10 +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.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Scoring; - namespace osu.Game.Rulesets.Osu.Objects { public class SliderRepeat : SliderEndCircle @@ -13,12 +9,5 @@ namespace osu.Game.Rulesets.Osu.Objects : base(slider) { } - - public override Judgement CreateJudgement() => new SliderRepeatJudgement(); - - public class SliderRepeatJudgement : OsuJudgement - { - public override HitResult MaxResult => HitResult.LargeTickHit; - } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 54d2afb444..357476ed30 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -9,16 +9,28 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SliderTailCircle : SliderEndCircle { + /// + /// Whether to treat this as a normal for judgement purposes. + /// If false, this will be judged as a instead. + /// + public bool ClassicSliderBehaviour; + public SliderTailCircle(Slider slider) : base(slider) { } - public override Judgement CreateJudgement() => new SliderTailJudgement(); + public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new LegacyTailJudgement() : new TailJudgement(); - public class SliderTailJudgement : OsuJudgement + public class LegacyTailJudgement : OsuJudgement { public override HitResult MaxResult => HitResult.SmallTickHit; } + + public class TailJudgement : SliderEndJudgement + { + public override HitResult MaxResult => HitResult.LargeTickHit; + public override HitResult MinResult => HitResult.IgnoreMiss; + } } } diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index fed338b012..6380b73558 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -350,6 +350,9 @@ namespace osu.Game.Rulesets.Scoring if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement."); + if (minResult == HitResult.IgnoreMiss) + return; + if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss) throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); From 704f5a6de3a4dba381efc0dc62a77ce1cb166f7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 19:40:20 +0900 Subject: [PATCH 474/896] Adjust sizing slightly to ensure we stay within 1366 limits --- .../Skinning/Argon/ManiaArgonSkinTransformer.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs index f74b7f1d02..ca7f84cb4d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs @@ -101,13 +101,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon case LegacyManiaSkinConfigurationLookups.ColumnWidth: - float width = 60; + float width; + + bool isSpecialColumn = stage.IsSpecialColumn(columnIndex); // Best effort until we have better mobile support. if (RuntimeInfo.IsMobile) - width = 180 * Math.Min(1, 7f / beatmap.TotalColumns); + width = 170 * Math.Min(1, 7f / beatmap.TotalColumns) * (isSpecialColumn ? 1.8f : 1); + else + width = 60 * (isSpecialColumn ? 2 : 1); - return SkinUtils.As(new Bindable(stage.IsSpecialColumn(columnIndex) ? width * 2 : width)); + return SkinUtils.As(new Bindable(width)); case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour: From f0f595ca40ab02cd0637a50ce0246d05423fc0c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 19:52:49 +0900 Subject: [PATCH 475/896] Continue to play spinner bonus sounds when MAX display occurs --- .../Objects/Drawables/DrawableSpinner.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2e9a4d92ec..aa43532f65 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -303,6 +303,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; + private int lastMaxSamplePlayback; + private void updateBonusScore() { if (ticks.Count == 0) @@ -322,7 +324,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var tick = ticks.FirstOrDefault(t => !t.Result.HasResult); // tick may be null if we've hit the spin limit. - tick?.TriggerResult(true); + if (tick == null) + { + // we still want to play a sound. this will probably be a new sound in the future, but for now let's continue playing the bonus sound. + // round robin to avoid hitting playback concurrency. + tick = ticks.OfType().Skip(lastMaxSamplePlayback++ % HitObject.MaximumBonusSpins).First(); + tick.PlaySamples(); + } + else + tick.TriggerResult(true); completedFullSpins.Value++; } From ac6fb386d137a13eae242fa65cf417e3d6ec4abb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Nov 2023 23:42:52 +0900 Subject: [PATCH 476/896] Remove nested ternary --- .../Objects/Drawables/DrawableSliderHead.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 8463c78319..83882481a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -65,14 +65,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Debug.Assert(HitObject != null); - return HitObject.ClassicSliderBehaviour - // In classic slider behaviour, heads are considered fully hit if in the largest hit window. + if (HitObject.ClassicSliderBehaviour) + { + // With classic slider behaviour, heads are considered fully hit if in the largest hit window. // We can't award a full Great because the true Great judgement is awarded on the Slider itself, // reduced based on number of ticks hit. // // so we use the most suitable LargeTick judgement here instead. - ? base.ResultFor(timeOffset).IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss - : base.ResultFor(timeOffset); + return base.ResultFor(timeOffset).IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; + } + + return base.ResultFor(timeOffset); } public override void Shake() From 86ede717cbeb705a7036276b49e5c53aeb6ecc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 18:52:47 +0100 Subject: [PATCH 477/896] Clean up comment --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 83882481a8..ff690417a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -69,8 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { // With classic slider behaviour, heads are considered fully hit if in the largest hit window. // We can't award a full Great because the true Great judgement is awarded on the Slider itself, - // reduced based on number of ticks hit. - // + // reduced based on number of ticks hit, // so we use the most suitable LargeTick judgement here instead. return base.ResultFor(timeOffset).IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; } From a0757ce13f3156c526d51b7d3be6afcd47436230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 18:58:14 +0100 Subject: [PATCH 478/896] Update xmldoc to match flipped flag semantics --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++-- osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 50791183f5..7f2d9592af 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -124,8 +124,8 @@ namespace osu.Game.Rulesets.Osu.Objects public double TickDistanceMultiplier = 1; /// - /// Whether this 's judgement is fully handled by its nested s. - /// If false, this will be judged proportionally to the number of nested s hit. + /// If , 's judgement is fully handled by its nested s. + /// If , this will be judged proportionally to the number of nested s hit. /// public bool ClassicSliderBehaviour { diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 4712d61dfe..8305481788 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -9,8 +9,8 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderHeadCircle : HitCircle { /// - /// Whether to treat this as a normal for judgement purposes. - /// If false, this will be judged as a instead. + /// If , treat this as a normal for judgement purposes. + /// If , this will be judged as a instead. /// public bool ClassicSliderBehaviour; From 9f11a04cc731159c6d2c0e4f7084238f6bd88360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 13:24:01 +0100 Subject: [PATCH 479/896] Generalise notion of 'touch device' mod --- osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs | 9 +-------- osu.Game/Rulesets/Mods/ModTouchDevice.cs | 16 ++++++++++++++++ osu.Game/Rulesets/Ruleset.cs | 2 ++ 3 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ModTouchDevice.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs index fd5c46a226..abb67c519b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs @@ -1,18 +1,11 @@ // 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.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModTouchDevice : Mod + public class OsuModTouchDevice : ModTouchDevice { - public override string Name => "Touch Device"; - public override string Acronym => "TD"; - public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; - public override double ScoreMultiplier => 1; - - public override ModType Type => ModType.System; } } diff --git a/osu.Game/Rulesets/Mods/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs new file mode 100644 index 0000000000..2daea8ea5d --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.cs @@ -0,0 +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.Framework.Localisation; + +namespace osu.Game.Rulesets.Mods +{ + public class ModTouchDevice : Mod + { + public sealed override string Name => "Touch Device"; + public sealed override string Acronym => "TD"; + public sealed override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; + public sealed override double ScoreMultiplier => 1; + public sealed override ModType Type => ModType.System; + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index be0d757e06..f8aa6c9f57 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -204,6 +204,8 @@ namespace osu.Game.Rulesets public ModAutoplay? GetAutoplayMod() => CreateMod(); + public ModTouchDevice? GetTouchDeviceMod() => CreateMod(); + /// /// Create a transformer which adds lookups specific to a ruleset to skin sources. /// From 68efb3c110aa3617fb801cb8afe6b7fb15afcba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 19:25:57 +0100 Subject: [PATCH 480/896] Mark `ModTouchDevice` as always valid for submission --- osu.Game/Rulesets/Mods/IMod.cs | 7 +++++++ osu.Game/Rulesets/Mods/Mod.cs | 4 ++++ osu.Game/Rulesets/Mods/ModTouchDevice.cs | 1 + 3 files changed, 12 insertions(+) diff --git a/osu.Game/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index ce2d123884..744d02a4fa 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -59,6 +59,13 @@ namespace osu.Game.Rulesets.Mods /// bool ValidForMultiplayerAsFreeMod { get; } + /// + /// Indicates that this mod is always permitted in scenarios wherein a user is submitting a score regardless of other circumstances. + /// Intended for mods that are informational in nature and do not really affect gameplay by themselves, + /// but are more of a gauge of increased/decreased difficulty due to the user's configuration (e.g. ). + /// + bool AlwaysValidForSubmission { get; } + /// /// Create a fresh instance based on this mod. /// diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 49c2fdd394..775f6a0ed4 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -156,6 +156,10 @@ namespace osu.Game.Rulesets.Mods [JsonIgnore] public virtual bool ValidForMultiplayerAsFreeMod => true; + /// + [JsonIgnore] + public virtual bool AlwaysValidForSubmission => false; + /// /// Whether this mod requires configuration to apply changes to the game. /// diff --git a/osu.Game/Rulesets/Mods/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs index 2daea8ea5d..0865603c8c 100644 --- a/osu.Game/Rulesets/Mods/ModTouchDevice.cs +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.cs @@ -12,5 +12,6 @@ namespace osu.Game.Rulesets.Mods public sealed override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; public sealed override double ScoreMultiplier => 1; public sealed override ModType Type => ModType.System; + public sealed override bool AlwaysValidForSubmission => true; } } From 980c900f43aa958e25d519e687a4a2e224d2399c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 14:08:56 +0100 Subject: [PATCH 481/896] Add component for game-wide touch detection --- .../Navigation/TestSceneScreenNavigation.cs | 22 ++++++++++ osu.Game/Configuration/SessionStatics.cs | 10 ++++- osu.Game/Input/TouchInputInterceptor.cs | 41 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 2 + 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Input/TouchInputInterceptor.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 9e743ef336..f4318da403 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -835,6 +836,27 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog is ConfirmExitDialog); } + [Test] + public void TestTouchScreenDetection() + { + AddStep("touch logo", () => + { + var button = Game.ChildrenOfType().Single(); + var touch = new Touch(TouchSource.Touch1, button.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch screen detected active", () => Game.Dependencies.Get().Get(Static.TouchInputActive), () => Is.True); + + AddStep("click settings button", () => + { + var button = Game.ChildrenOfType().Last(); + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + AddAssert("touch screen detected inactive", () => Game.Dependencies.Get().Get(Static.TouchInputActive), () => Is.False); + } + private Func playToResults() { var player = playToCompletion(); diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 5e2f0c2128..0fc2076a7e 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Game.Graphics.UserInterface; +using osu.Game.Input; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -24,6 +25,7 @@ namespace osu.Game.Configuration SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); + SetDefault(Static.TouchInputActive, false); } /// @@ -63,6 +65,12 @@ namespace osu.Game.Configuration /// The last playback time in milliseconds of an on/off sample (from ). /// Used to debounce on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods. /// - LastModSelectPanelSamplePlaybackTime + LastModSelectPanelSamplePlaybackTime, + + /// + /// Whether the last positional input received was a touch input. + /// Used in touchscreen detection scenarios (). + /// + TouchInputActive, } } diff --git a/osu.Game/Input/TouchInputInterceptor.cs b/osu.Game/Input/TouchInputInterceptor.cs new file mode 100644 index 0000000000..377d3c6d9f --- /dev/null +++ b/osu.Game/Input/TouchInputInterceptor.cs @@ -0,0 +1,41 @@ +// 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.Input.Events; +using osu.Framework.Input.StateChanges; +using osu.Game.Configuration; +using osuTK; + +namespace osu.Game.Input +{ + /// + /// Intercepts all positional input events and sets the appropriate value + /// for consumption by particular game screens. + /// + public partial class TouchInputInterceptor : Component + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + + [Resolved] + private SessionStatics statics { get; set; } = null!; + + protected override bool Handle(UIEvent e) + { + switch (e) + { + case MouseEvent: + if (e.CurrentState.Mouse.LastSource is not ISourcedFromTouch) + statics.SetValue(Static.TouchInputActive, false); + break; + + case TouchEvent: + statics.SetValue(Static.TouchInputActive, true); + break; + } + + return false; + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1f46eb0c0d..e4376d1c02 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -407,6 +407,8 @@ namespace osu.Game }) }); + base.Content.Add(new TouchInputInterceptor()); + KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); From 2f6ff893b550a18ec1f4791d45c96c5dcb3e99cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 14:38:20 +0100 Subject: [PATCH 482/896] Automatically activate and deactivate touch device mod in song select --- .../Navigation/TestSceneScreenNavigation.cs | 42 ++++++++++++++ osu.Game/Rulesets/Mods/ModTouchDevice.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 1 + .../Select/SongSelectTouchInputHandler.cs | 56 +++++++++++++++++++ osu.Game/Utils/ModUtils.cs | 2 +- 5 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Screens/Select/SongSelectTouchInputHandler.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index f4318da403..0ba8f7136d 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -855,6 +855,48 @@ namespace osu.Game.Tests.Visual.Navigation InputManager.Click(MouseButton.Left); }); AddAssert("touch screen detected inactive", () => Game.Dependencies.Get().Get(Static.TouchInputActive), () => Is.False); + + AddStep("close settings sidebar", () => InputManager.Key(Key.Escape)); + + PushAndConfirm(() => new TestPlaySongSelect()); + AddStep("switch to osu! ruleset", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.Number1); + InputManager.ReleaseKey(Key.LControl); + }); + AddStep("touch beatmap wedge", () => + { + var wedge = Game.ChildrenOfType().Single(); + var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); + + AddStep("switch to mania ruleset", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.Number4); + InputManager.ReleaseKey(Key.LControl); + }); + AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); + AddStep("touch beatmap wedge", () => + { + var wedge = Game.ChildrenOfType().Single(); + var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); + + AddStep("switch to osu! ruleset", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.Number1); + InputManager.ReleaseKey(Key.LControl); + }); + AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); } private Func playToResults() diff --git a/osu.Game/Rulesets/Mods/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs index 0865603c8c..f532e7b19d 100644 --- a/osu.Game/Rulesets/Mods/ModTouchDevice.cs +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.cs @@ -5,7 +5,7 @@ using osu.Framework.Localisation; namespace osu.Game.Rulesets.Mods { - public class ModTouchDevice : Mod + public class ModTouchDevice : Mod, IApplicableMod { public sealed override string Name => "Touch Device"; public sealed override string Acronym => "TD"; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index dfea4e3794..74454713d1 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -279,6 +279,7 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.Both, }, + new SongSelectTouchInputHandler() }); if (ShowFooter) diff --git a/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs b/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs new file mode 100644 index 0000000000..bf4761e871 --- /dev/null +++ b/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs @@ -0,0 +1,56 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.Select +{ + public partial class SongSelectTouchInputHandler : Component + { + [Resolved] + private Bindable ruleset { get; set; } = null!; + + [Resolved] + private Bindable> mods { get; set; } = null!; + + private IBindable touchActive = null!; + + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + touchActive = statics.GetBindable(Static.TouchInputActive); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => updateState()); + mods.BindValueChanged(_ => updateState()); + touchActive.BindValueChanged(_ => updateState()); + updateState(); + } + + private void updateState() + { + var touchDeviceMod = ruleset.Value.CreateInstance().GetTouchDeviceMod(); + + if (touchDeviceMod == null) + return; + + bool touchDeviceModEnabled = mods.Value.Any(mod => mod is ModTouchDevice); + + if (touchActive.Value && !touchDeviceModEnabled) + mods.Value = mods.Value.Append(touchDeviceMod).ToArray(); + if (!touchActive.Value && touchDeviceModEnabled) + mods.Value = mods.Value.Where(mod => mod is not ModTouchDevice).ToArray(); + } + } +} diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index edf9cc80da..1bd60fcdde 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -121,7 +121,7 @@ namespace osu.Game.Utils if (!CheckCompatibleSet(mods, out invalidMods)) return false; - return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods); + return checkValid(mods, m => m.HasImplementation, out invalidMods); } /// From 29d4d81eaab886e0f99bd2c579a1425a41b4b17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 25 Oct 2023 19:56:51 +0200 Subject: [PATCH 483/896] Add test scene for basic touchscreen detection scenarios --- .../Mods/TestSceneOsuModTouchDevice.cs | 134 ++++++++++++++++++ .../Navigation/TestSceneScreenNavigation.cs | 5 +- 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs new file mode 100644 index 0000000000..9c84bcc241 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Overlays; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public partial class TestSceneOsuModTouchDevice : PlayerTestScene + { + private TestOnScreenDisplay testOnScreenDisplay = null!; + + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => + new OsuBeatmap + { + HitObjects = + { + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 0, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 5000, + }, + }, + Breaks = + { + new BreakPeriod(2000, 3000) + } + }; + + [BackgroundDependencyLoader] + private void load() + { + Add(testOnScreenDisplay = new TestOnScreenDisplay()); + Dependencies.CacheAs(testOnScreenDisplay); + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("reset OSD toast count", () => testOnScreenDisplay.ToastCount = 0); + } + + [Test] + public void TestFirstObjectTouched() + { + AddUntilStep("wait until 0 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0).Within(500)); + AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); + AddUntilStep("wait until 0", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + AddStep("touch circle", () => + { + var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + AddAssert("no toasts displayed", () => testOnScreenDisplay.ToastCount, () => Is.Zero); + } + + [Test] + public void TestTouchDuringBreak() + { + AddUntilStep("wait until 2000 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000).Within(500)); + AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); + AddUntilStep("wait until 2000", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000)); + AddStep("touch playfield", () => + { + var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod not activated", () => Player.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); + AddAssert("no toasts displayed", () => testOnScreenDisplay.ToastCount, () => Is.Zero); + } + + [Test] + public void TestSecondObjectTouched() + { + // ensure mouse is active (and that it's not suppressed due to touches in previous tests) + AddStep("click mouse", () => InputManager.Click(MouseButton.Left)); + + AddUntilStep("wait until 0 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0).Within(500)); + AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); + AddUntilStep("wait until 0", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + AddStep("click circle", () => + { + InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + }); + AddAssert("touch device mod not activated", () => Player.Mods.Value, () => Has.No.InstanceOf()); + + AddStep("speed back up", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 1); + AddUntilStep("wait until 5000 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000).Within(500)); + AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); + AddUntilStep("wait until 5000", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000)); + AddStep("touch playfield", () => + { + var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + AddAssert("toast displayed", () => testOnScreenDisplay.ToastCount, () => Is.EqualTo(1)); + } + + private partial class TestOnScreenDisplay : OnScreenDisplay + { + public int ToastCount { get; set; } + + protected override void DisplayTemporarily(Drawable toDisplay) + { + base.DisplayTemporarily(toDisplay); + ToastCount++; + } + } + } +} diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 0ba8f7136d..10c1104376 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -858,7 +858,10 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("close settings sidebar", () => InputManager.Key(Key.Escape)); - PushAndConfirm(() => new TestPlaySongSelect()); + Screens.Select.SongSelect songSelect = null; + PushAndConfirm(() => songSelect = new TestPlaySongSelect()); + AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + AddStep("switch to osu! ruleset", () => { InputManager.PressKey(Key.LControl); From f2df02b60f7e78a4e1fad2c591c24dfe14271c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 25 Oct 2023 20:27:07 +0200 Subject: [PATCH 484/896] Automatically activate touch device mod in player --- .../Mods/TestSceneOsuModTouchDevice.cs | 18 +++++- .../Overlays/OSD/TouchDeviceDetectedToast.cs | 13 ++++ osu.Game/Screens/Play/Player.cs | 1 + .../Screens/Play/PlayerTouchInputHandler.cs | 61 +++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs create mode 100644 osu.Game/Screens/Play/PlayerTouchInputHandler.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index 9c84bcc241..5134265741 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -1,12 +1,15 @@ // 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.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; +using osu.Game.Configuration; +using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; @@ -19,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { public partial class TestSceneOsuModTouchDevice : PlayerTestScene { + [Resolved] + private SessionStatics statics { get; set; } = null!; + private TestOnScreenDisplay testOnScreenDisplay = null!; protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); @@ -49,18 +55,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private void load() { Add(testOnScreenDisplay = new TestOnScreenDisplay()); + Add(new TouchInputInterceptor()); Dependencies.CacheAs(testOnScreenDisplay); } public override void SetUpSteps() { - base.SetUpSteps(); AddStep("reset OSD toast count", () => testOnScreenDisplay.ToastCount = 0); + AddStep("reset static", () => statics.SetValue(Static.TouchInputActive, false)); + base.SetUpSteps(); } [Test] - public void TestFirstObjectTouched() + public void TestUserAlreadyHasTouchDeviceActive() { + // it is presumed that a previous screen (i.e. song select) will set this up + AddStep("set up touchscreen user", () => + { + Player.Score.ScoreInfo.Mods = Player.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray(); + statics.SetValue(Static.TouchInputActive, true); + }); AddUntilStep("wait until 0 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0).Within(500)); AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); AddUntilStep("wait until 0", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); diff --git a/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs b/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs new file mode 100644 index 0000000000..0b5e3fffbe --- /dev/null +++ b/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs @@ -0,0 +1,13 @@ +// 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.Overlays.OSD +{ + public partial class TouchDeviceDetectedToast : Toast + { + public TouchDeviceDetectedToast() + : base("osu!", "Touch device detected", "Touch Device mod applied to score") + { + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8c7fc551ba..a67e16912d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -285,6 +285,7 @@ namespace osu.Game.Screens.Play fadeOut(true); }, }, + new PlayerTouchInputHandler() }); if (cancellationToken.IsCancellationRequested) diff --git a/osu.Game/Screens/Play/PlayerTouchInputHandler.cs b/osu.Game/Screens/Play/PlayerTouchInputHandler.cs new file mode 100644 index 0000000000..0252ee9503 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerTouchInputHandler.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 System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Overlays; +using osu.Game.Overlays.OSD; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.Play +{ + public partial class PlayerTouchInputHandler : Component + { + [Resolved] + private Player player { get; set; } = null!; + + [Resolved] + private GameplayState gameplayState { get; set; } = null!; + + [Resolved] + private OnScreenDisplay? onScreenDisplay { get; set; } + + private IBindable touchActive = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + touchActive = statics.GetBindable(Static.TouchInputActive); + touchActive.BindValueChanged(_ => updateState()); + } + + private void updateState() + { + if (!touchActive.Value) + return; + + if (gameplayState.HasPassed || gameplayState.HasFailed || gameplayState.HasQuit) + return; + + if (gameplayState.Score.ScoreInfo.Mods.OfType().Any()) + return; + + if (player.IsBreakTime.Value) + return; + + var touchDeviceMod = gameplayState.Ruleset.GetTouchDeviceMod(); + if (touchDeviceMod == null) + return; + + onScreenDisplay?.Display(new TouchDeviceDetectedToast()); + + // TODO: this is kinda crude. `Player` (probably rightly so) assumes immutability of mods. + // this probably should be shown immediately on screen in the HUD, + // which means that immutability will probably need to be revisited. + player.Score.ScoreInfo.Mods = player.Score.ScoreInfo.Mods.Append(touchDeviceMod).ToArray(); + } + } +} From 21a4da463b11d557a815581cdb09121a9ea0a94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 13:47:50 +0100 Subject: [PATCH 485/896] Add failing test covering crash scenario --- .../Navigation/TestSceneScreenNavigation.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 10c1104376..a1fd8768b6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -900,6 +900,38 @@ namespace osu.Game.Tests.Visual.Navigation InputManager.ReleaseKey(Key.LControl); }); AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); + + AddStep("click beatmap wedge", () => + { + InputManager.MoveMouseTo(Game.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + Player player = null; + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return (player = Game.ScreenStack.CurrentScreen as Player) != null; + }); + + AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning); + + AddStep("touch", () => + { + var touch = new Touch(TouchSource.Touch2, Game.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddUntilStep("touch device mod added to score", () => player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + + AddStep("exit player", () => player.Exit()); + AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf()); } private Func playToResults() From ef555ed0cf442b98cb139de435e0e0815468c05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Oct 2023 15:48:00 +0100 Subject: [PATCH 486/896] Fix test failures --- osu.Game.Tests/Mods/ModUtilsTest.cs | 6 +++--- .../Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs | 1 + osu.Game/Overlays/Mods/SelectAllModsButton.cs | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index aa41fd830b..9107ddd1ae 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -147,11 +147,11 @@ namespace osu.Game.Tests.Mods new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() }, new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) } }, - // system mod. + // system mod not applicable in lazer. new object[] { - new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, - new[] { typeof(OsuModTouchDevice) } + new Mod[] { new OsuModHidden(), new ModScoreV2() }, + new[] { typeof(ModScoreV2) } }, // multi mod. new object[] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 66ba908879..f1674401cd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -133,6 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private bool assertAllAvailableModsSelected() { var allAvailableMods = availableMods.Value + .Where(pair => pair.Key != ModType.System) .SelectMany(pair => pair.Value) .Where(mod => mod.UserPlayable && mod.HasImplementation) .ToList(); diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index bb61cdc35d..b6b3051a0d 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -41,6 +41,7 @@ namespace osu.Game.Overlays.Mods private void updateEnabledState() { Enabled.Value = availableMods.Value + .Where(pair => pair.Key != ModType.System) .SelectMany(pair => pair.Value) .Any(modState => !modState.Active.Value && modState.Visible); } From c588f434e5f51bee3683c6114bc951fda40fb50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 31 Oct 2023 13:54:18 +0100 Subject: [PATCH 487/896] Fix song select touch handler causing crashes when song select is suspended --- osu.Game/Screens/Select/SongSelectTouchInputHandler.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs b/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs index bf4761e871..5c27e59c5b 100644 --- a/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs +++ b/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs @@ -32,14 +32,18 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - ruleset.BindValueChanged(_ => updateState()); - mods.BindValueChanged(_ => updateState()); - touchActive.BindValueChanged(_ => updateState()); + ruleset.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + mods.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + mods.BindDisabledChanged(_ => Scheduler.AddOnce(updateState)); + touchActive.BindValueChanged(_ => Scheduler.AddOnce(updateState)); updateState(); } private void updateState() { + if (mods.Disabled) + return; + var touchDeviceMod = ruleset.Value.CreateInstance().GetTouchDeviceMod(); if (touchDeviceMod == null) From 4532d0ecdf12abd32b3d64cbab3b9e975f34c63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 19:48:34 +0100 Subject: [PATCH 488/896] Add logging & debug facility for touch input interceptor --- osu.Game/Input/TouchInputInterceptor.cs | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game/Input/TouchInputInterceptor.cs b/osu.Game/Input/TouchInputInterceptor.cs index 377d3c6d9f..4e6177c70c 100644 --- a/osu.Game/Input/TouchInputInterceptor.cs +++ b/osu.Game/Input/TouchInputInterceptor.cs @@ -1,12 +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 System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; +using osu.Framework.Logging; using osu.Game.Configuration; using osuTK; +using osuTK.Input; namespace osu.Game.Input { @@ -23,19 +27,42 @@ namespace osu.Game.Input protected override bool Handle(UIEvent e) { + bool touchInputWasActive = statics.Get(Static.TouchInputActive); + switch (e) { case MouseEvent: if (e.CurrentState.Mouse.LastSource is not ISourcedFromTouch) + { + if (touchInputWasActive) + Logger.Log($@"Touch input deactivated due to received {e.GetType().ReadableName()}", LoggingTarget.Input); statics.SetValue(Static.TouchInputActive, false); + } + break; case TouchEvent: + if (!touchInputWasActive) + Logger.Log($@"Touch input activated due to received {e.GetType().ReadableName()}", LoggingTarget.Input); statics.SetValue(Static.TouchInputActive, true); break; + + case KeyDownEvent keyDown: + if (keyDown.Key == Key.T && keyDown.ControlPressed && keyDown.ShiftPressed) + debugToggleTouchInputActive(); + break; } return false; } + + [Conditional("TOUCH_INPUT_DEBUG")] + private void debugToggleTouchInputActive() + { + bool oldValue = statics.Get(Static.TouchInputActive); + bool newValue = !oldValue; + Logger.Log($@"Debug-toggling touch input to {(newValue ? @"active" : @"inactive")}", LoggingTarget.Input, LogLevel.Debug); + statics.SetValue(Static.TouchInputActive, newValue); + } } } From 3a6d65d395c1bf3c0e30429fec90be5e4fedb7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 19:51:21 +0100 Subject: [PATCH 489/896] Touch up `PlayerTouchInputHandler` --- osu.Game/Screens/Play/PlayerTouchInputHandler.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerTouchInputHandler.cs b/osu.Game/Screens/Play/PlayerTouchInputHandler.cs index 0252ee9503..728b3c846b 100644 --- a/osu.Game/Screens/Play/PlayerTouchInputHandler.cs +++ b/osu.Game/Screens/Play/PlayerTouchInputHandler.cs @@ -9,6 +9,7 @@ using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Overlays.OSD; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play { @@ -50,11 +51,15 @@ namespace osu.Game.Screens.Play if (touchDeviceMod == null) return; - onScreenDisplay?.Display(new TouchDeviceDetectedToast()); + // do not show the toast if the user hasn't hit anything yet. + // we're kind of assuming that the user just switches to touch for gameplay + // and we don't want to spam them with obvious toasts. + if (gameplayState.ScoreProcessor.HitEvents.Any(ev => ev.Result.IsHit())) + onScreenDisplay?.Display(new TouchDeviceDetectedToast()); - // TODO: this is kinda crude. `Player` (probably rightly so) assumes immutability of mods. - // this probably should be shown immediately on screen in the HUD, - // which means that immutability will probably need to be revisited. + // `Player` (probably rightly so) assumes immutability of mods, + // so this will not be shown immediately on the mod display in the top right. + // if this is to change, the mod immutability should be revisited. player.Score.ScoreInfo.Mods = player.Score.ScoreInfo.Mods.Append(touchDeviceMod).ToArray(); } } From d25b54c06d660455484762a011c162c6cb68dfd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 22:14:38 +0100 Subject: [PATCH 490/896] Split test into two They would fail on CI when written as one, and I don't care why. --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index a1fd8768b6..c6a668a714 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -837,7 +837,7 @@ namespace osu.Game.Tests.Visual.Navigation } [Test] - public void TestTouchScreenDetection() + public void TestTouchScreenDetectionAtSongSelect() { AddStep("touch logo", () => { @@ -859,8 +859,9 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("close settings sidebar", () => InputManager.Key(Key.Escape)); Screens.Select.SongSelect songSelect = null; - PushAndConfirm(() => songSelect = new TestPlaySongSelect()); - AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); + AddRepeatStep("go to solo", () => InputManager.Key(Key.P), 3); + AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect) != null); + AddUntilStep("wait for beatmap sets loaded", () => songSelect.BeatmapSetsLoaded); AddStep("switch to osu! ruleset", () => { @@ -907,10 +908,15 @@ namespace osu.Game.Tests.Visual.Navigation InputManager.Click(MouseButton.Left); }); AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf()); + } + [Test] + public void TestTouchScreenDetectionInGame() + { + PushAndConfirm(() => new TestPlaySongSelect()); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); - AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddStep("select", () => InputManager.Key(Key.Enter)); Player player = null; From 8784dd42c0c4c1aba37cb0cc4f5cda8055811a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 22:20:17 +0100 Subject: [PATCH 491/896] Do not attempt to turn on Touch Device in song select with autoplay active --- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModTouchDevice.cs | 2 ++ .../Screens/Select/SongSelectTouchInputHandler.cs | 11 ++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 973fcffba8..302cdf69c0 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mods public sealed override bool ValidForMultiplayer => false; public sealed override bool ValidForMultiplayerAsFreeMod => false; - public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) }; + public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed), typeof(ModTouchDevice) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs index f532e7b19d..c29ff28f1a 100644 --- a/osu.Game/Rulesets/Mods/ModTouchDevice.cs +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.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.Localisation; namespace osu.Game.Rulesets.Mods @@ -13,5 +14,6 @@ namespace osu.Game.Rulesets.Mods public sealed override double ScoreMultiplier => 1; public sealed override ModType Type => ModType.System; public sealed override bool AlwaysValidForSubmission => true; + public sealed override Type[] IncompatibleMods => new[] { typeof(ICreateReplayData) }; } } diff --git a/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs b/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs index 5c27e59c5b..973dc12e12 100644 --- a/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs +++ b/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Utils; namespace osu.Game.Screens.Select { @@ -52,7 +53,15 @@ namespace osu.Game.Screens.Select bool touchDeviceModEnabled = mods.Value.Any(mod => mod is ModTouchDevice); if (touchActive.Value && !touchDeviceModEnabled) - mods.Value = mods.Value.Append(touchDeviceMod).ToArray(); + { + var candidateMods = mods.Value.Append(touchDeviceMod).ToArray(); + + if (!ModUtils.CheckCompatibleSet(candidateMods, out _)) + return; + + mods.Value = candidateMods; + } + if (!touchActive.Value && touchDeviceModEnabled) mods.Value = mods.Value.Where(mod => mod is not ModTouchDevice).ToArray(); } From 0dd0a84312e224bb2cb5396cdc479e2b04688000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 22:26:52 +0100 Subject: [PATCH 492/896] Move player touch handler to `SubmittingPlayer` Shouldn't be there in `ReplayPlayer`. --- osu.Game/Screens/Play/Player.cs | 1 - osu.Game/Screens/Play/SubmittingPlayer.cs | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a67e16912d..8c7fc551ba 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -285,7 +285,6 @@ namespace osu.Game.Screens.Play fadeOut(true); }, }, - new PlayerTouchInputHandler() }); if (cancellationToken.IsCancellationRequested) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index a75546f835..c34902aeb8 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -44,6 +44,12 @@ namespace osu.Game.Screens.Play { } + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new PlayerTouchInputHandler()); + } + protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); From 8e9006b5d5c263cf79fed1e74532e7de5dfca122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 22:27:16 +0100 Subject: [PATCH 493/896] Declare Touch Device incompatible with Autopilot With Autopilot active, Touch Device no longer matters. --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs | 3 +++ osu.Game/Rulesets/Mods/ModTouchDevice.cs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 3841c9c716..56bf0e08e9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -33,7 +33,8 @@ namespace osu.Game.Rulesets.Osu.Mods typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), - typeof(OsuModRepel) + typeof(OsuModRepel), + typeof(ModTouchDevice) }; public bool PerformFail() => false; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs index abb67c519b..f1468d414e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTouchDevice.cs @@ -1,11 +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.Linq; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModTouchDevice : ModTouchDevice { + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray(); } } diff --git a/osu.Game/Rulesets/Mods/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs index c29ff28f1a..a5dfe5448c 100644 --- a/osu.Game/Rulesets/Mods/ModTouchDevice.cs +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Mods public sealed override double ScoreMultiplier => 1; public sealed override ModType Type => ModType.System; public sealed override bool AlwaysValidForSubmission => true; - public sealed override Type[] IncompatibleMods => new[] { typeof(ICreateReplayData) }; + public override Type[] IncompatibleMods => new[] { typeof(ICreateReplayData) }; } } From a613292802ce0fe5f53b31de761e6dcdf3135113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 2 Nov 2023 23:44:40 +0100 Subject: [PATCH 494/896] Fix unknown mod test failure --- osu.Game/Screens/Play/SubmittingPlayer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index c34902aeb8..e018b8dab3 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -47,6 +47,12 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { + if (DrawableRuleset == null) + { + // base load must have failed (e.g. due to an unknown mod); bail. + return; + } + AddInternal(new PlayerTouchInputHandler()); } From a78fab0e7d74d41b58ecefbe74854370ffee08c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 3 Nov 2023 00:17:29 +0100 Subject: [PATCH 495/896] Do not hardcode ruleset name in touch device detection toast --- osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs | 6 ++++-- osu.Game/Screens/Play/PlayerTouchInputHandler.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs b/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs index 0b5e3fffbe..266e10ab1f 100644 --- a/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs +++ b/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.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.Game.Rulesets; + namespace osu.Game.Overlays.OSD { public partial class TouchDeviceDetectedToast : Toast { - public TouchDeviceDetectedToast() - : base("osu!", "Touch device detected", "Touch Device mod applied to score") + public TouchDeviceDetectedToast(RulesetInfo ruleset) + : base(ruleset.Name, "Touch device detected", "Touch Device mod applied to score") { } } diff --git a/osu.Game/Screens/Play/PlayerTouchInputHandler.cs b/osu.Game/Screens/Play/PlayerTouchInputHandler.cs index 728b3c846b..a7d41de105 100644 --- a/osu.Game/Screens/Play/PlayerTouchInputHandler.cs +++ b/osu.Game/Screens/Play/PlayerTouchInputHandler.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Play // we're kind of assuming that the user just switches to touch for gameplay // and we don't want to spam them with obvious toasts. if (gameplayState.ScoreProcessor.HitEvents.Any(ev => ev.Result.IsHit())) - onScreenDisplay?.Display(new TouchDeviceDetectedToast()); + onScreenDisplay?.Display(new TouchDeviceDetectedToast(gameplayState.Ruleset.RulesetInfo)); // `Player` (probably rightly so) assumes immutability of mods, // so this will not be shown immediately on the mod display in the top right. From 090601b485bdec297ef3d723f7254b54ca335d3b Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 Nov 2023 18:12:39 -0700 Subject: [PATCH 496/896] Apply peppy's upright key counter attempt diff Co-Authored-By: Dean Herbert --- .../Visual/Gameplay/TestSceneKeyCounter.cs | 20 ++++++++- osu.Game/Screens/Play/ArgonKeyCounter.cs | 45 ++++++++++++------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 5a66a5c7a6..7e66106264 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -43,7 +43,25 @@ namespace osu.Game.Tests.Visual.Gameplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, - } + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = -90, + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = 90, + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Scale = new Vector2(1, -1) + }, } } }; diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs index 2d725898d8..ebf53abb30 100644 --- a/osu.Game/Screens/Play/ArgonKeyCounter.cs +++ b/osu.Game/Screens/Play/ArgonKeyCounter.cs @@ -3,8 +3,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Play.HUD; using osuTK; @@ -40,26 +42,39 @@ namespace osu.Game.Screens.Play { inputIndicator = new Circle { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, Height = line_height * scale_factor, Alpha = 0.5f }, - keyNameText = new OsuSpriteText + new Container { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(0, -13) * scale_factor, - Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold), - Colour = colours.Blue0, - Text = Trigger.Name - }, - countText = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold), + RelativeSizeAxes = Axes.X, + Height = 40, + Children = new Drawable[] + { + new UprightAspectMaintainingContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + keyNameText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold), + Colour = colours.Blue0, + Text = Trigger.Name + }, + countText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Position = new Vector2(0, 13) * scale_factor, + Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold), + }, + } + } + } }, }; From b3dfe19472a2a6a7f7d2b2d34570036df8f6b49c Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 Nov 2023 18:17:25 -0700 Subject: [PATCH 497/896] Fix `UprightAspectMaintainingContainer` not being centred --- osu.Game/Screens/Play/ArgonKeyCounter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs index ebf53abb30..6ff60c68b7 100644 --- a/osu.Game/Screens/Play/ArgonKeyCounter.cs +++ b/osu.Game/Screens/Play/ArgonKeyCounter.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.Play new UprightAspectMaintainingContainer { RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Children = new Drawable[] { keyNameText = new OsuSpriteText From 16731ff85ff90b8c9e20b40b0358bc2c128782c2 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 Nov 2023 18:20:15 -0700 Subject: [PATCH 498/896] Fix text positioning --- osu.Game/Screens/Play/ArgonKeyCounter.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs index 6ff60c68b7..bb5fe0daf2 100644 --- a/osu.Game/Screens/Play/ArgonKeyCounter.cs +++ b/osu.Game/Screens/Play/ArgonKeyCounter.cs @@ -9,7 +9,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Play.HUD; -using osuTK; namespace osu.Game.Screens.Play { @@ -27,6 +26,8 @@ namespace osu.Game.Screens.Play // Make things look bigger without using Scale private const float scale_factor = 1.5f; + private const float indicator_press_offset = 4; + [Resolved] private OsuColour colours { get; set; } = null!; @@ -48,8 +49,8 @@ namespace osu.Game.Screens.Play }, new Container { - RelativeSizeAxes = Axes.X, - Height = 40, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = line_height * scale_factor + indicator_press_offset }, Children = new Drawable[] { new UprightAspectMaintainingContainer @@ -69,9 +70,8 @@ namespace osu.Game.Screens.Play }, countText = new OsuSpriteText { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Position = new Vector2(0, 13) * scale_factor, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold), }, } @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Play .FadeIn(10, Easing.OutQuint) .MoveToY(0) .Then() - .MoveToY(4, 60, Easing.OutQuint); + .MoveToY(indicator_press_offset, 60, Easing.OutQuint); } protected override void Deactivate(bool forwardPlayback = true) From e3b3ce6c84102e34778c4fd25bb39e366dc0977f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 Nov 2023 18:44:56 -0700 Subject: [PATCH 499/896] Fix test overflowing on widescreen + add default (triangles) key counter testing --- .../Visual/Gameplay/TestSceneKeyCounter.cs | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 7e66106264..03302bae6a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -31,30 +31,24 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, - Spacing = new Vector2(72.7f), - Children = new KeyCounterDisplay[] + Spacing = new Vector2(20), + Children = new Drawable[] { new DefaultKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, }, - new ArgonKeyCounterDisplay + new DefaultKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, + Scale = new Vector2(1, -1) }, new ArgonKeyCounterDisplay { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Rotation = -90, - }, - new ArgonKeyCounterDisplay - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Rotation = 90, }, new ArgonKeyCounterDisplay { @@ -62,6 +56,41 @@ namespace osu.Game.Tests.Visual.Gameplay Anchor = Anchor.Centre, Scale = new Vector2(1, -1) }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new DefaultKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = -90, + }, + new DefaultKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = 90, + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = -90, + }, + new ArgonKeyCounterDisplay + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Rotation = 90, + }, + } + }, } } }; From 3f8baf913b46b884d9cbb5530721d58913403b5a Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Thu, 2 Nov 2023 19:46:09 -0700 Subject: [PATCH 500/896] Add 100 and 1000 key press step to test overflow --- osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index 03302bae6a..2d2b6c3bed 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -124,8 +124,15 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Disable counting", () => controller.IsCounting.Value = false); addPressKeyStep(); AddAssert($"Check {testKey} count has not changed", () => testTrigger.ActivationCount.Value == 2); + AddStep("Enable counting", () => controller.IsCounting.Value = true); + addPressKeyStep(100); + addPressKeyStep(1000); - void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); + void addPressKeyStep(int repeat = 1) => AddStep($"Press {testKey} key {repeat} times", () => + { + for (int i = 0; i < repeat; i++) + InputManager.Key(testKey); + }); } } } From 43ab7f49426626b3d9536af481221c98e7dca86a Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 4 Nov 2023 02:01:18 +0100 Subject: [PATCH 501/896] Added OpenEditorTimestamp base implementation --- osu.Game/Localisation/EditorStrings.cs | 15 +++ osu.Game/OsuGame.cs | 48 +++++++++ osu.Game/Screens/Edit/Editor.cs | 34 ++++++ .../Screens/Edit/EditorTimestampParser.cs | 101 ++++++++++++++++++ 4 files changed, 198 insertions(+) create mode 100644 osu.Game/Screens/Edit/EditorTimestampParser.cs diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 93e52746c5..227dbc5e0c 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -119,6 +119,21 @@ namespace osu.Game.Localisation /// public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time"); + /// + /// "Must be in edit mode to handle editor links" + /// + public static LocalisableString MustBeInEdit => new TranslatableString(getKey(@"must_be_in_edit"), @"Must be in edit mode to handle editor links"); + + /// + /// "Failed to process timestamp" + /// + public static LocalisableString FailedToProcessTimestamp => new TranslatableString(getKey(@"failed_to_process_timestamp"), @"Failed to process timestamp"); + + /// + /// "The timestamp was too long to process" + /// + public static LocalisableString TooLongTimestamp => new TranslatableString(getKey(@"too_long_timestamp"), @"The timestamp was too long to process"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2f11964f6a..439e112bd3 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -58,6 +58,7 @@ using osu.Game.Performance; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; +using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Play; @@ -433,6 +434,9 @@ namespace osu.Game break; case LinkAction.OpenEditorTimestamp: + SeekToTimestamp(argString); + break; + case LinkAction.JoinMultiplayerMatch: case LinkAction.Spectate: waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification @@ -550,6 +554,50 @@ namespace osu.Game /// The build version of the update stream public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version)); + /// + /// Seek to a given timestamp in the Editor and select relevant HitObjects if needed + /// + /// The timestamp and the selected objects + public void SeekToTimestamp(string timestamp) + { + if (ScreenStack.CurrentScreen is not Editor editor) + { + waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification + { + Text = EditorStrings.MustBeInEdit, + })); + return; + } + + string[] groups = EditorTimestampParser.GetRegexGroups(timestamp); + + if (groups.Length != 2 || string.IsNullOrEmpty(groups[0])) + { + waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification + { + Text = EditorStrings.FailedToProcessTimestamp + })); + return; + } + + string timeGroup = groups[0]; + string objectsGroup = groups[1]; + string timeMinutes = timeGroup.Split(':').FirstOrDefault() ?? string.Empty; + + // Currently, lazer chat highlights infinite-long editor links like `10000000000:00:000 (1)` + // Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues + if (timeMinutes.Length > 5 || double.Parse(timeMinutes) > 30_000) + { + waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification + { + Text = EditorStrings.TooLongTimestamp + })); + return; + } + + editor.SeekToTimestamp(timeGroup, objectsGroup); + } + /// /// Present a skin select immediately. /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 91c3c98f01..b2fad55fed 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -39,6 +39,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Overlays.OSD; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -1137,6 +1138,39 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } + public void SeekToTimestamp(string timeGroup, string objectsGroup) + { + double position = EditorTimestampParser.GetTotalMilliseconds(timeGroup); + editorBeatmap.SelectedHitObjects.Clear(); + + if (string.IsNullOrEmpty(objectsGroup)) + { + if (clock.IsRunning) + clock.Stop(); + + clock.Seek(position); + return; + } + + if (Mode.Value != EditorScreenMode.Compose) + Mode.Value = EditorScreenMode.Compose; + + // Seek to the next closest HitObject's position + HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position); + if (nextObject != null && nextObject.StartTime > 0) + position = nextObject.StartTime; + + List selected = EditorTimestampParser.GetSelectedHitObjects(editorBeatmap.HitObjects.ToList(), objectsGroup, position); + + if (selected.Any()) + editorBeatmap.SelectedHitObjects.AddRange(selected); + + if (clock.IsRunning) + clock.Stop(); + + clock.Seek(position); + } + public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); diff --git a/osu.Game/Screens/Edit/EditorTimestampParser.cs b/osu.Game/Screens/Edit/EditorTimestampParser.cs new file mode 100644 index 0000000000..44d614ca70 --- /dev/null +++ b/osu.Game/Screens/Edit/EditorTimestampParser.cs @@ -0,0 +1,101 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Screens.Edit +{ + public static class EditorTimestampParser + { + private static readonly Regex timestamp_regex = new Regex(@"^(\d+:\d+:\d+)(?: \((\d+(?:[|,]\d+)*)\))?$", RegexOptions.Compiled); + + public static string[] GetRegexGroups(string timestamp) + { + Match match = timestamp_regex.Match(timestamp); + return match.Success + ? match.Groups.Values.Where(x => x is not Match).Select(x => x.Value).ToArray() + : Array.Empty(); + } + + public static double GetTotalMilliseconds(string timeGroup) + { + int[] times = timeGroup.Split(':').Select(int.Parse).ToArray(); + + Debug.Assert(times.Length == 3); + + return (times[0] * 60 + times[1]) * 1_000 + times[2]; + } + + public static List GetSelectedHitObjects(IEnumerable editorHitObjects, string objectsGroup, double position) + { + List hitObjects = editorHitObjects.Where(x => x.StartTime >= position).ToList(); + List selectedObjects = new List(); + + string[] objectsToSelect = objectsGroup.Split(',').ToArray(); + + foreach (string objectInfo in objectsToSelect) + { + HitObject? current = hitObjects.FirstOrDefault(x => shouldHitObjectBeSelected(x, objectInfo)); + + if (current == null) + continue; + + selectedObjects.Add(current); + hitObjects = hitObjects.Where(x => x != current && x.StartTime >= current.StartTime).ToList(); + } + + // Stable behavior + // - always selects next closest object when `objectsGroup` only has one, non-Column item + if (objectsToSelect.Length != 1 || objectsGroup.Contains('|')) + return selectedObjects; + + HitObject? nextClosest = editorHitObjects.FirstOrDefault(x => x.StartTime >= position); + if (nextClosest == null) + return selectedObjects; + + if (nextClosest.StartTime <= (selectedObjects.FirstOrDefault()?.StartTime ?? position)) + { + selectedObjects.Clear(); + selectedObjects.Add(nextClosest); + } + + return selectedObjects; + } + + private static bool shouldHitObjectBeSelected(HitObject hitObject, string objectInfo) + { + switch (hitObject) + { + // (combo) + case IHasComboInformation comboInfo: + { + if (!double.TryParse(objectInfo, out double comboValue) || comboValue < 1) + return false; + + return comboInfo.IndexInCurrentCombo + 1 == comboValue; + } + + // (time|column) + case IHasColumn column: + { + double[] split = objectInfo.Split('|').Select(double.Parse).ToArray(); + if (split.Length != 2) + return false; + + double timeValue = split[0]; + double columnValue = split[1]; + return hitObject.StartTime == timeValue && column.Column == columnValue; + } + + default: + return false; + } + } + } +} From f854e78bb03f6117b1cfb8b0579e867d1aae093f Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 4 Nov 2023 03:29:05 +0100 Subject: [PATCH 502/896] Added ExclamationTriangle Icon to notifications --- osu.Game/OsuGame.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 439e112bd3..5acd958568 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -562,8 +562,9 @@ namespace osu.Game { if (ScreenStack.CurrentScreen is not Editor editor) { - waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification + waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification { + Icon = FontAwesome.Solid.ExclamationTriangle, Text = EditorStrings.MustBeInEdit, })); return; @@ -573,8 +574,9 @@ namespace osu.Game if (groups.Length != 2 || string.IsNullOrEmpty(groups[0])) { - waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification + waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification { + Icon = FontAwesome.Solid.ExclamationTriangle, Text = EditorStrings.FailedToProcessTimestamp })); return; @@ -588,8 +590,9 @@ namespace osu.Game // Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues if (timeMinutes.Length > 5 || double.Parse(timeMinutes) > 30_000) { - waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification + waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification { + Icon = FontAwesome.Solid.ExclamationTriangle, Text = EditorStrings.TooLongTimestamp })); return; From 60f62faec3712e1086af4d0a7462c60d0efbc257 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 4 Nov 2023 03:30:38 +0100 Subject: [PATCH 503/896] Renamed Editor method --- osu.Game/OsuGame.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5acd958568..a6bb6cc120 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -598,7 +598,7 @@ namespace osu.Game return; } - editor.SeekToTimestamp(timeGroup, objectsGroup); + editor.SeekAndSelectHitObjects(timeGroup, objectsGroup); } /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b2fad55fed..80e01d4eb7 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1138,7 +1138,7 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } - public void SeekToTimestamp(string timeGroup, string objectsGroup) + public void SeekAndSelectHitObjects(string timeGroup, string objectsGroup) { double position = EditorTimestampParser.GetTotalMilliseconds(timeGroup); editorBeatmap.SelectedHitObjects.Clear(); From f867cff8c79c55f75708ea41c699c6d7a557485c Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 4 Nov 2023 03:42:08 +0100 Subject: [PATCH 504/896] Added OpenEditorTimestamp Tests --- .../Editing/TestSceneOpenEditorTimestamp.cs | 377 ++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs new file mode 100644 index 0000000000..5ae0a20fd2 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -0,0 +1,377 @@ +// 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.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Localisation; +using osu.Game.Online.Chat; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestSceneOpenEditorTimestamp : OsuGameTestScene + { + protected Editor Editor => (Editor)Game.ScreenStack.CurrentScreen; + protected EditorBeatmap EditorBeatmap => Editor.ChildrenOfType().Single(); + protected EditorClock EditorClock => Editor.ChildrenOfType().Single(); + + protected void AddStepClickLink(string timestamp, string step = "") + { + AddStep(step + timestamp, () => + Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp)) + ); + } + + protected void AddStepScreenModeTo(EditorScreenMode screenMode) + { + AddStep("change screen to " + screenMode, () => Editor.Mode.Value = screenMode); + } + + protected void AssertOnScreenAt(EditorScreenMode screen, double time, string text = "stays in") + { + AddAssert($"{text} {screen} at {time}", () => + Editor.Mode.Value == screen + && EditorClock.CurrentTime == time + ); + } + + protected bool HasCombosInOrder(IEnumerable selected, params int[] comboNumbers) + { + List hitObjects = selected.ToList(); + if (hitObjects.Count != comboNumbers.Length) + return false; + + return !hitObjects.Select(x => (IHasComboInformation)x) + .Where((combo, i) => combo.IndexInCurrentCombo + 1 != comboNumbers[i]) + .Any(); + } + + protected bool IsNoteAt(HitObject hitObject, double time, int column) + { + return hitObject is IHasColumn columnInfo + && hitObject.StartTime == time + && columnInfo.Column == column; + } + + public void SetUpEditor(RulesetInfo ruleset) + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("Import test beatmap", () => + Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely() + ); + AddStep("Retrieve beatmap", () => + beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach() + ); + AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("Wait for song select", () => + Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded + ); + AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset); + AddStep("Open editor for ruleset", () => + ((PlaySongSelect)Game.ScreenStack.CurrentScreen) + .Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name)) + ); + AddUntilStep("Wait for editor open", () => Editor.ReadyForUse); + } + + [Test] + public void TestErrorNotifications() + { + RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; + + AddStepClickLink("00:00:000"); + AddAssert("recieved 'must be in edit'", () => + Game.Notifications.UnreadCount.Value == 1 + && Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 1 + ); + + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + + AddStepClickLink("00:00:000 (1)"); + AddAssert("recieved 'must be in edit'", () => + Game.Notifications.UnreadCount.Value == 2 + && Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 2 + ); + + SetUpEditor(rulesetInfo); + AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + + AddStepClickLink("00:000", "invalid link "); + AddAssert("recieved 'failed to process'", () => + Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 1 + ); + + AddStepClickLink("00:00:00:000", "invalid link "); + AddAssert("recieved 'failed to process'", () => + Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 2 + ); + + AddStepClickLink("00:00:000 ()", "invalid link "); + AddAssert("recieved 'failed to process'", () => + Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 3 + ); + + AddStepClickLink("00:00:000 (-1)", "invalid link "); + AddAssert("recieved 'failed to process'", () => + Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 4 + ); + + AddStepClickLink("50000:00:000", "too long link "); + AddAssert("recieved 'too long'", () => + EditorClock.CurrentTime == 0 + && Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.TooLongTimestamp) == 1 + ); + } + + [Test] + public void TestHandleCurrentScreenChanges() + { + const long long_link_value = 1_000 * 60 * 1_000; + RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; + + SetUpEditor(rulesetInfo); + AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + + AddStepClickLink("1000:00:000", "long link "); + AddAssert("moved to end of track", () => + EditorClock.CurrentTime == long_link_value + || (EditorClock.TrackLength < long_link_value && EditorClock.CurrentTime == EditorClock.TrackLength) + ); + + AddStepScreenModeTo(EditorScreenMode.SongSetup); + AddStepClickLink("00:00:000"); + AssertOnScreenAt(EditorScreenMode.SongSetup, 0); + + AddStepClickLink("00:05:000 (0|0)"); + AddAssert("seek and change screen", () => + Editor.Mode.Value == EditorScreenMode.Compose + && EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 5_000).StartTime + ); + + AddStepScreenModeTo(EditorScreenMode.Design); + AddStepClickLink("00:10:000"); + AssertOnScreenAt(EditorScreenMode.Design, 10_000); + + AddStepClickLink("00:15:000 (1)"); + AddAssert("seek and change screen", () => + Editor.Mode.Value == EditorScreenMode.Compose + && EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 15_000).StartTime + ); + + AddStepScreenModeTo(EditorScreenMode.Timing); + AddStepClickLink("00:20:000"); + AssertOnScreenAt(EditorScreenMode.Timing, 20_000); + + AddStepClickLink("00:25:000 (0,1)"); + AddAssert("seek and change screen", () => + Editor.Mode.Value == EditorScreenMode.Compose + && EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 25_000).StartTime + ); + + AddStepScreenModeTo(EditorScreenMode.Verify); + AddStepClickLink("00:30:000"); + AssertOnScreenAt(EditorScreenMode.Verify, 30_000); + + AddStepClickLink("00:35:000 (0,1)"); + AddAssert("seek and change screen", () => + Editor.Mode.Value == EditorScreenMode.Compose + && EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 35_000).StartTime + ); + + AddStepClickLink("00:00:000"); + AssertOnScreenAt(EditorScreenMode.Compose, 0); + } + + [Test] + public void TestSelectionForOsu() + { + HitObject firstObject = null!; + RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; + + SetUpEditor(rulesetInfo); + AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + + AddStepClickLink("00:00:956 (1,2,3)"); + AddAssert("snap and select 1-2-3", () => + { + firstObject = EditorBeatmap.HitObjects.First(); + return EditorClock.CurrentTime == firstObject.StartTime + && EditorBeatmap.SelectedHitObjects.Count == 3 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1, 2, 3); + }); + + AddStepClickLink("00:01:450 (2,3,4,1,2)"); + AddAssert("snap and select 2-3-4-1-2", () => + EditorClock.CurrentTime == 1_450 + && EditorBeatmap.SelectedHitObjects.Count == 5 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 2, 3, 4, 1, 2) + ); + + AddStepClickLink("00:00:956 (1,1,1)"); + AddAssert("snap and select 1-1-1", () => + EditorClock.CurrentTime == firstObject.StartTime + && EditorBeatmap.SelectedHitObjects.Count == 3 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1, 1, 1) + ); + } + + [Test] + public void TestUnusualSelectionForOsu() + { + HitObject firstObject = null!; + RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; + + SetUpEditor(rulesetInfo); + AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + + AddStepClickLink("00:00:000 (1,2,3)", "invalid offset "); + AddAssert("snap to next, select 1-2-3", () => + { + firstObject = EditorBeatmap.HitObjects.First(); + return EditorClock.CurrentTime == firstObject.StartTime + && EditorBeatmap.SelectedHitObjects.Count == 3 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1, 2, 3); + }); + + AddStepClickLink("00:00:956 (2,3,4)", "invalid offset "); + AddAssert("snap to next, select 2-3-4", () => + EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 956).StartTime + && EditorBeatmap.SelectedHitObjects.Count == 3 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 2, 3, 4) + ); + + AddStepClickLink("00:00:000 (0)", "invalid combo "); + AddAssert("snap to 1, select 1", () => + EditorClock.CurrentTime == firstObject.StartTime + && EditorBeatmap.SelectedHitObjects.Count == 1 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1) + ); + + AddStepClickLink("00:00:000 (1)", "invalid offset "); + AddAssert("snap and select 1", () => + EditorClock.CurrentTime == firstObject.StartTime + && EditorBeatmap.SelectedHitObjects.Count == 1 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1) + ); + + AddStepClickLink("00:00:000 (2)", "invalid offset "); + AddAssert("snap and select 1", () => + EditorClock.CurrentTime == firstObject.StartTime + && EditorBeatmap.SelectedHitObjects.Count == 1 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1) + ); + + AddStepClickLink("00:00:000 (2,3)", "invalid offset "); + AddAssert("snap to 1, select 2-3", () => + EditorClock.CurrentTime == firstObject.StartTime + && EditorBeatmap.SelectedHitObjects.Count == 2 + && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 2, 3) + ); + + AddStepClickLink("00:00:956 (956|1,956|2)", "mania link "); + AddAssert("snap to next, select none", () => + EditorClock.CurrentTime == firstObject?.StartTime + && !EditorBeatmap.SelectedHitObjects.Any() + ); + + AddStepClickLink("00:00:000 (0|1)", "mania link "); + AddAssert("snap to 1, select none", () => + EditorClock.CurrentTime == firstObject.StartTime + && !EditorBeatmap.SelectedHitObjects.Any() + ); + } + + [Test] + public void TestSelectionForMania() + { + RulesetInfo rulesetInfo = new ManiaRuleset().RulesetInfo; + + SetUpEditor(rulesetInfo); + AddAssert("is editor Mania", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + + AddStepClickLink("00:11:010 (11010|1,11175|5,11258|3,11340|5,11505|1)"); + AddAssert("selected group", () => + EditorClock.CurrentTime == 11010 + && EditorBeatmap.SelectedHitObjects.Count == 5 + && EditorBeatmap.SelectedHitObjects.All(x => + IsNoteAt(x, 11010, 1) || IsNoteAt(x, 11175, 5) || + IsNoteAt(x, 11258, 3) || IsNoteAt(x, 11340, 5) || + IsNoteAt(x, 11505, 1)) + ); + + AddStepClickLink("00:00:956 (956|1,956|6,1285|3,1780|4)"); + AddAssert("selected ungrouped", () => + EditorClock.CurrentTime == 956 + && EditorBeatmap.SelectedHitObjects.Count == 4 + && EditorBeatmap.SelectedHitObjects.All(x => + IsNoteAt(x, 956, 1) || IsNoteAt(x, 956, 6) || + IsNoteAt(x, 1285, 3) || IsNoteAt(x, 1780, 4)) + ); + + AddStepClickLink("02:36:560 (156560|1,156560|4,156560|6)"); + AddAssert("selected in row", () => + EditorClock.CurrentTime == 156560 + && EditorBeatmap.SelectedHitObjects.Count == 3 + && EditorBeatmap.SelectedHitObjects.All(x => + IsNoteAt(x, 156560, 1) || IsNoteAt(x, 156560, 4) || + IsNoteAt(x, 156560, 6)) + ); + + AddStepClickLink("00:35:736 (35736|3,36395|3,36725|3,37384|3)"); + AddAssert("selected in column", () => + EditorClock.CurrentTime == 35736 + && EditorBeatmap.SelectedHitObjects.Count == 4 + && EditorBeatmap.SelectedHitObjects.All(x => + IsNoteAt(x, 35736, 3) || IsNoteAt(x, 36395, 3) || + IsNoteAt(x, 36725, 3) || IsNoteAt(x, 37384, 3)) + ); + } + + [Test] + public void TestUnusualSelectionForMania() + { + RulesetInfo rulesetInfo = new ManiaRuleset().RulesetInfo; + + SetUpEditor(rulesetInfo); + AddAssert("is editor Mania", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + + AddStepClickLink("00:00:000 (0|1)", "invalid link "); + AddAssert("snap to 1, select none", () => + EditorClock.CurrentTime == 956 + && !EditorBeatmap.SelectedHitObjects.Any() + ); + + AddStepClickLink("00:00:000 (0)", "std link "); + AddAssert("snap and select 1", () => + EditorClock.CurrentTime == 956 + && EditorBeatmap.SelectedHitObjects.All(x => IsNoteAt(x, 956, 1)) + ); + + AddStepClickLink("00:00:000 (1,2)", "std link "); + AddAssert("snap to 1, select none", () => + EditorClock.CurrentTime == 956 + && !EditorBeatmap.SelectedHitObjects.Any() + ); + } + } +} From 19cdf99df8f101b5c676528826a39cb905c1efe5 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 4 Nov 2023 12:04:58 +0100 Subject: [PATCH 505/896] Fixed up tests --- .../Editing/TestSceneOpenEditorTimestamp.cs | 216 +++++++----------- 1 file changed, 83 insertions(+), 133 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index 5ae0a20fd2..fea9334ff8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Editing protected void AddStepClickLink(string timestamp, string step = "") { - AddStep(step + timestamp, () => + AddStep($"{step} {timestamp}", () => Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp)) ); } @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("change screen to " + screenMode, () => Editor.Mode.Value = screenMode); } - protected void AssertOnScreenAt(EditorScreenMode screen, double time, string text = "stays in") + protected void AssertOnScreenAt(EditorScreenMode screen, double time, string text = "stayed in") { AddAssert($"{text} {screen} at {time}", () => Editor.Mode.Value == screen @@ -51,7 +51,34 @@ namespace osu.Game.Tests.Visual.Editing ); } - protected bool HasCombosInOrder(IEnumerable selected, params int[] comboNumbers) + protected void AssertMovedScreenTo(EditorScreenMode screen, string text = "moved to") + { + AddAssert($"{text} {screen}", () => Editor.Mode.Value == screen); + } + + private bool checkSnapAndSelectCombo(double startTime, params int[] comboNumbers) + { + bool checkCombos = comboNumbers.Any() + ? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime + && EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length + && checkCombos; + } + + private bool checkSnapAndSelectColumn(double startTime, List<(int, int)> columnPairs = null) + { + bool checkColumns = columnPairs != null + ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime + && EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0) + && checkColumns; + } + + private bool hasCombosInOrder(IEnumerable selected, params int[] comboNumbers) { List hitObjects = selected.ToList(); if (hitObjects.Count != comboNumbers.Length) @@ -62,7 +89,7 @@ namespace osu.Game.Tests.Visual.Editing .Any(); } - protected bool IsNoteAt(HitObject hitObject, double time, int column) + private bool isNoteAt(HitObject hitObject, double time, int column) { return hitObject is IHasColumn columnInfo && hitObject.StartTime == time @@ -100,8 +127,7 @@ namespace osu.Game.Tests.Visual.Editing AddStepClickLink("00:00:000"); AddAssert("recieved 'must be in edit'", () => - Game.Notifications.UnreadCount.Value == 1 - && Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 1 + Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 1 ); AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); @@ -109,37 +135,35 @@ namespace osu.Game.Tests.Visual.Editing AddStepClickLink("00:00:000 (1)"); AddAssert("recieved 'must be in edit'", () => - Game.Notifications.UnreadCount.Value == 2 - && Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 2 + Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 2 ); SetUpEditor(rulesetInfo); AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - AddStepClickLink("00:000", "invalid link "); + AddStepClickLink("00:000", "invalid link"); AddAssert("recieved 'failed to process'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 1 ); - AddStepClickLink("00:00:00:000", "invalid link "); + AddStepClickLink("00:00:00:000", "invalid link"); AddAssert("recieved 'failed to process'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 2 ); - AddStepClickLink("00:00:000 ()", "invalid link "); + AddStepClickLink("00:00:000 ()", "invalid link"); AddAssert("recieved 'failed to process'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 3 ); - AddStepClickLink("00:00:000 (-1)", "invalid link "); + AddStepClickLink("00:00:000 (-1)", "invalid link"); AddAssert("recieved 'failed to process'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 4 ); - AddStepClickLink("50000:00:000", "too long link "); + AddStepClickLink("50000:00:000", "too long link"); AddAssert("recieved 'too long'", () => - EditorClock.CurrentTime == 0 - && Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.TooLongTimestamp) == 1 + Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.TooLongTimestamp) == 1 ); } @@ -152,7 +176,7 @@ namespace osu.Game.Tests.Visual.Editing SetUpEditor(rulesetInfo); AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - AddStepClickLink("1000:00:000", "long link "); + AddStepClickLink("1000:00:000", "long link"); AddAssert("moved to end of track", () => EditorClock.CurrentTime == long_link_value || (EditorClock.TrackLength < long_link_value && EditorClock.CurrentTime == EditorClock.TrackLength) @@ -163,40 +187,28 @@ namespace osu.Game.Tests.Visual.Editing AssertOnScreenAt(EditorScreenMode.SongSetup, 0); AddStepClickLink("00:05:000 (0|0)"); - AddAssert("seek and change screen", () => - Editor.Mode.Value == EditorScreenMode.Compose - && EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 5_000).StartTime - ); + AssertMovedScreenTo(EditorScreenMode.Compose); AddStepScreenModeTo(EditorScreenMode.Design); AddStepClickLink("00:10:000"); AssertOnScreenAt(EditorScreenMode.Design, 10_000); AddStepClickLink("00:15:000 (1)"); - AddAssert("seek and change screen", () => - Editor.Mode.Value == EditorScreenMode.Compose - && EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 15_000).StartTime - ); + AssertMovedScreenTo(EditorScreenMode.Compose); AddStepScreenModeTo(EditorScreenMode.Timing); AddStepClickLink("00:20:000"); AssertOnScreenAt(EditorScreenMode.Timing, 20_000); AddStepClickLink("00:25:000 (0,1)"); - AddAssert("seek and change screen", () => - Editor.Mode.Value == EditorScreenMode.Compose - && EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 25_000).StartTime - ); + AssertMovedScreenTo(EditorScreenMode.Compose); AddStepScreenModeTo(EditorScreenMode.Verify); AddStepClickLink("00:30:000"); AssertOnScreenAt(EditorScreenMode.Verify, 30_000); AddStepClickLink("00:35:000 (0,1)"); - AddAssert("seek and change screen", () => - Editor.Mode.Value == EditorScreenMode.Compose - && EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 35_000).StartTime - ); + AssertMovedScreenTo(EditorScreenMode.Compose); AddStepClickLink("00:00:000"); AssertOnScreenAt(EditorScreenMode.Compose, 0); @@ -215,24 +227,14 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("snap and select 1-2-3", () => { firstObject = EditorBeatmap.HitObjects.First(); - return EditorClock.CurrentTime == firstObject.StartTime - && EditorBeatmap.SelectedHitObjects.Count == 3 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1, 2, 3); + return checkSnapAndSelectCombo(firstObject.StartTime, 1, 2, 3); }); AddStepClickLink("00:01:450 (2,3,4,1,2)"); - AddAssert("snap and select 2-3-4-1-2", () => - EditorClock.CurrentTime == 1_450 - && EditorBeatmap.SelectedHitObjects.Count == 5 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 2, 3, 4, 1, 2) - ); + AddAssert("snap and select 2-3-4-1-2", () => checkSnapAndSelectCombo(1_450, 2, 3, 4, 1, 2)); AddStepClickLink("00:00:956 (1,1,1)"); - AddAssert("snap and select 1-1-1", () => - EditorClock.CurrentTime == firstObject.StartTime - && EditorBeatmap.SelectedHitObjects.Count == 3 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1, 1, 1) - ); + AddAssert("snap and select 1-1-1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1, 1, 1)); } [Test] @@ -244,61 +246,33 @@ namespace osu.Game.Tests.Visual.Editing SetUpEditor(rulesetInfo); AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - AddStepClickLink("00:00:000 (1,2,3)", "invalid offset "); + AddStepClickLink("00:00:000 (1,2,3)", "invalid offset"); AddAssert("snap to next, select 1-2-3", () => { firstObject = EditorBeatmap.HitObjects.First(); - return EditorClock.CurrentTime == firstObject.StartTime - && EditorBeatmap.SelectedHitObjects.Count == 3 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1, 2, 3); + return checkSnapAndSelectCombo(firstObject.StartTime, 1, 2, 3); }); - AddStepClickLink("00:00:956 (2,3,4)", "invalid offset "); - AddAssert("snap to next, select 2-3-4", () => - EditorClock.CurrentTime == EditorBeatmap.HitObjects.First(x => x.StartTime >= 956).StartTime - && EditorBeatmap.SelectedHitObjects.Count == 3 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 2, 3, 4) - ); + AddStepClickLink("00:00:956 (2,3,4)", "invalid offset"); + AddAssert("snap to next, select 2-3-4", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3, 4)); - AddStepClickLink("00:00:000 (0)", "invalid combo "); - AddAssert("snap to 1, select 1", () => - EditorClock.CurrentTime == firstObject.StartTime - && EditorBeatmap.SelectedHitObjects.Count == 1 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1) - ); + AddStepClickLink("00:00:000 (0)", "invalid offset"); + AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - AddStepClickLink("00:00:000 (1)", "invalid offset "); - AddAssert("snap and select 1", () => - EditorClock.CurrentTime == firstObject.StartTime - && EditorBeatmap.SelectedHitObjects.Count == 1 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1) - ); + AddStepClickLink("00:00:000 (1)", "invalid offset"); + AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - AddStepClickLink("00:00:000 (2)", "invalid offset "); - AddAssert("snap and select 1", () => - EditorClock.CurrentTime == firstObject.StartTime - && EditorBeatmap.SelectedHitObjects.Count == 1 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 1) - ); + AddStepClickLink("00:00:000 (2)", "invalid offset"); + AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - AddStepClickLink("00:00:000 (2,3)", "invalid offset "); - AddAssert("snap to 1, select 2-3", () => - EditorClock.CurrentTime == firstObject.StartTime - && EditorBeatmap.SelectedHitObjects.Count == 2 - && HasCombosInOrder(EditorBeatmap.SelectedHitObjects, 2, 3) - ); + AddStepClickLink("00:00:000 (2,3)", "invalid offset"); + AddAssert("snap to 1, select 2-3", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3)); - AddStepClickLink("00:00:956 (956|1,956|2)", "mania link "); - AddAssert("snap to next, select none", () => - EditorClock.CurrentTime == firstObject?.StartTime - && !EditorBeatmap.SelectedHitObjects.Any() - ); + AddStepClickLink("00:00:956 (956|1,956|2)", "mania link"); + AddAssert("snap to next, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); - AddStepClickLink("00:00:000 (0|1)", "mania link "); - AddAssert("snap to 1, select none", () => - EditorClock.CurrentTime == firstObject.StartTime - && !EditorBeatmap.SelectedHitObjects.Any() - ); + AddStepClickLink("00:00:000 (0|1)", "mania link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); } [Test] @@ -310,41 +284,24 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("is editor Mania", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); AddStepClickLink("00:11:010 (11010|1,11175|5,11258|3,11340|5,11505|1)"); - AddAssert("selected group", () => - EditorClock.CurrentTime == 11010 - && EditorBeatmap.SelectedHitObjects.Count == 5 - && EditorBeatmap.SelectedHitObjects.All(x => - IsNoteAt(x, 11010, 1) || IsNoteAt(x, 11175, 5) || - IsNoteAt(x, 11258, 3) || IsNoteAt(x, 11340, 5) || - IsNoteAt(x, 11505, 1)) - ); + AddAssert("selected group", () => checkSnapAndSelectColumn(11010, new List<(int, int)> + { (11010, 1), (11175, 5), (11258, 3), (11340, 5), (11505, 1) } + )); AddStepClickLink("00:00:956 (956|1,956|6,1285|3,1780|4)"); - AddAssert("selected ungrouped", () => - EditorClock.CurrentTime == 956 - && EditorBeatmap.SelectedHitObjects.Count == 4 - && EditorBeatmap.SelectedHitObjects.All(x => - IsNoteAt(x, 956, 1) || IsNoteAt(x, 956, 6) || - IsNoteAt(x, 1285, 3) || IsNoteAt(x, 1780, 4)) - ); + AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(956, new List<(int, int)> + { (956, 1), (956, 6), (1285, 3), (1780, 4) } + )); AddStepClickLink("02:36:560 (156560|1,156560|4,156560|6)"); - AddAssert("selected in row", () => - EditorClock.CurrentTime == 156560 - && EditorBeatmap.SelectedHitObjects.Count == 3 - && EditorBeatmap.SelectedHitObjects.All(x => - IsNoteAt(x, 156560, 1) || IsNoteAt(x, 156560, 4) || - IsNoteAt(x, 156560, 6)) - ); + AddAssert("selected in row", () => checkSnapAndSelectColumn(156560, new List<(int, int)> + { (156560, 1), (156560, 4), (156560, 6) } + )); AddStepClickLink("00:35:736 (35736|3,36395|3,36725|3,37384|3)"); - AddAssert("selected in column", () => - EditorClock.CurrentTime == 35736 - && EditorBeatmap.SelectedHitObjects.Count == 4 - && EditorBeatmap.SelectedHitObjects.All(x => - IsNoteAt(x, 35736, 3) || IsNoteAt(x, 36395, 3) || - IsNoteAt(x, 36725, 3) || IsNoteAt(x, 37384, 3)) - ); + AddAssert("selected in column", () => checkSnapAndSelectColumn(35736, new List<(int, int)> + { (35736, 3), (36395, 3), (36725, 3), (37384, 3) } + )); } [Test] @@ -355,23 +312,16 @@ namespace osu.Game.Tests.Visual.Editing SetUpEditor(rulesetInfo); AddAssert("is editor Mania", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - AddStepClickLink("00:00:000 (0|1)", "invalid link "); - AddAssert("snap to 1, select none", () => - EditorClock.CurrentTime == 956 - && !EditorBeatmap.SelectedHitObjects.Any() + AddStepClickLink("00:00:000 (0|1)", "invalid link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(956)); + + AddStepClickLink("00:00:000 (0)", "std link"); + AddAssert("snap and select 1", () => checkSnapAndSelectColumn(956, new List<(int, int)> + { (956, 1) }) ); - AddStepClickLink("00:00:000 (0)", "std link "); - AddAssert("snap and select 1", () => - EditorClock.CurrentTime == 956 - && EditorBeatmap.SelectedHitObjects.All(x => IsNoteAt(x, 956, 1)) - ); - - AddStepClickLink("00:00:000 (1,2)", "std link "); - AddAssert("snap to 1, select none", () => - EditorClock.CurrentTime == 956 - && !EditorBeatmap.SelectedHitObjects.Any() - ); + AddStepClickLink("00:00:000 (1,2)", "std link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(956)); } } } From a70bfca501312bc587c230064a6269b20a714c82 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Thu, 2 Nov 2023 09:16:25 +0100 Subject: [PATCH 506/896] added usergrid for tooltip --- osu.Game/Users/Drawables/ClickableAvatar.cs | 33 ++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 677a8fff36..0ed9f56cc7 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -3,16 +3,27 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; +using osuTK; namespace osu.Game.Users.Drawables { - public partial class ClickableAvatar : OsuClickableContainer + public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip { + public ITooltip GetCustomTooltip() => new UserGridPanelTooltip(); + + public UserGridPanel TooltipContent => new UserGridPanel(user!) + { + Width = 300 + }; + public override LocalisableString TooltipText { get @@ -67,5 +78,25 @@ namespace osu.Game.Users.Drawables return base.OnClick(e); } + + private partial class UserGridPanelTooltip : VisibilityContainer, ITooltip + { + private UserGridPanel? displayedUser; + + protected override void PopIn() + { + Child = displayedUser; + this.FadeIn(20, Easing.OutQuint); + } + + protected override void PopOut() => this.FadeOut(80, Easing.OutQuint); + + public void Move(Vector2 pos) => Position = pos; + + public void SetContent(UserGridPanel userGridPanel) + { + displayedUser = userGridPanel; + } + } } } From ec290ae953c9dffec44bb71e1fcde4e0785ef843 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Sat, 4 Nov 2023 19:03:23 +0100 Subject: [PATCH 507/896] added tests --- .../Online/TestSceneUserClickableAvatar.cs | 125 ++++++++++++++++++ osu.Game/Users/Drawables/ClickableAvatar.cs | 21 --- osu.Game/Users/Drawables/UpdateableAvatar.cs | 1 - 3 files changed, 125 insertions(+), 22 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs new file mode 100644 index 0000000000..13f559ac09 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -0,0 +1,125 @@ +// 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.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Testing; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Online +{ + public partial class TestSceneUserClickableAvatar : OsuManualInputManagerTestScene + { + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(10f), + Children = new Drawable[] + { + new ClickableAvatar(new APIUser + { + Username = @"flyte", Id = 3103765, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }) + { + Width = 50, + Height = 50, + CornerRadius = 10, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), + }, + }, + new ClickableAvatar(new APIUser + { + Username = @"peppy", Id = 2, Colour = "99EB47", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }) + { + Width = 50, + Height = 50, + CornerRadius = 10, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), + }, + }, + new ClickableAvatar(new APIUser + { + Username = @"flyte", + Id = 3103765, + CountryCode = CountryCode.JP, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", + Status = + { + Value = new UserStatusOnline() + } + }) + { + Width = 50, + Height = 50, + CornerRadius = 10, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), + }, + }, + }, + }; + }); + + [Test] + public void TestClickableAvatarHover() + { + AddStep($"click {1}. {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 1) + return; + + InputManager.MoveMouseTo(targets[0]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); + + AddStep($"click {2}. {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 2) + return; + + InputManager.MoveMouseTo(targets[1]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); + + AddStep($"click {3}. {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 3) + return; + + InputManager.MoveMouseTo(targets[2]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); + } + } +} diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 0ed9f56cc7..d6c6afba0b 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -1,15 +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.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; -using osu.Framework.Localisation; using osu.Game.Graphics.Containers; -using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -24,24 +21,6 @@ namespace osu.Game.Users.Drawables Width = 300 }; - public override LocalisableString TooltipText - { - get - { - if (!Enabled.Value) - return string.Empty; - - return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile; - } - set => throw new NotSupportedException(); - } - - /// - /// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username. - /// Setting this to true exposes the username via tooltip for special cases where this is not true. - /// - public bool ShowUsernameTooltip { get; set; } - private readonly APIUser? user; [Resolved] diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index c659685807..58b3646995 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -74,7 +74,6 @@ namespace osu.Game.Users.Drawables { return new ClickableAvatar(user) { - ShowUsernameTooltip = showUsernameTooltip, RelativeSizeAxes = Axes.Both, }; } From 7492d953aee303732df79ba0267f34dbacfbb3e9 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 4 Nov 2023 21:17:58 +0100 Subject: [PATCH 508/896] Moved error checks into Editor - Invoke Action on error to Notify user - added some comments --- .../Editing/TestSceneOpenEditorTimestamp.cs | 25 ++++++------ osu.Game/OsuGame.cs | 39 ++++--------------- osu.Game/Screens/Edit/Editor.cs | 27 ++++++++++++- .../Screens/Edit/EditorTimestampParser.cs | 4 +- 4 files changed, 47 insertions(+), 48 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index fea9334ff8..f65aff922e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -67,17 +67,6 @@ namespace osu.Game.Tests.Visual.Editing && checkCombos; } - private bool checkSnapAndSelectColumn(double startTime, List<(int, int)> columnPairs = null) - { - bool checkColumns = columnPairs != null - ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) - : !EditorBeatmap.SelectedHitObjects.Any(); - - return EditorClock.CurrentTime == startTime - && EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0) - && checkColumns; - } - private bool hasCombosInOrder(IEnumerable selected, params int[] comboNumbers) { List hitObjects = selected.ToList(); @@ -89,6 +78,17 @@ namespace osu.Game.Tests.Visual.Editing .Any(); } + private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)> columnPairs = null) + { + bool checkColumns = columnPairs != null + ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime + && EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0) + && checkColumns; + } + private bool isNoteAt(HitObject hitObject, double time, int column) { return hitObject is IHasColumn columnInfo @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Editing && columnInfo.Column == column; } - public void SetUpEditor(RulesetInfo ruleset) + protected void SetUpEditor(RulesetInfo ruleset) { BeatmapSetInfo beatmapSet = null!; @@ -320,6 +320,7 @@ namespace osu.Game.Tests.Visual.Editing { (956, 1) }) ); + // TODO: discuss - this selects the first 2 objects on Stable, do we want that or is this fine? AddStepClickLink("00:00:000 (1,2)", "std link"); AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(956)); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a6bb6cc120..a9d4927e33 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -562,43 +562,18 @@ namespace osu.Game { if (ScreenStack.CurrentScreen is not Editor editor) { - waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification - { - Icon = FontAwesome.Solid.ExclamationTriangle, - Text = EditorStrings.MustBeInEdit, - })); + postNotification(EditorStrings.MustBeInEdit); return; } - string[] groups = EditorTimestampParser.GetRegexGroups(timestamp); + editor.SeekAndSelectHitObjects(timestamp, onError: postNotification); + return; - if (groups.Length != 2 || string.IsNullOrEmpty(groups[0])) + void postNotification(LocalisableString message) => Schedule(() => Notifications.Post(new SimpleNotification { - waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification - { - Icon = FontAwesome.Solid.ExclamationTriangle, - Text = EditorStrings.FailedToProcessTimestamp - })); - return; - } - - string timeGroup = groups[0]; - string objectsGroup = groups[1]; - string timeMinutes = timeGroup.Split(':').FirstOrDefault() ?? string.Empty; - - // Currently, lazer chat highlights infinite-long editor links like `10000000000:00:000 (1)` - // Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues - if (timeMinutes.Length > 5 || double.Parse(timeMinutes) > 30_000) - { - waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification - { - Icon = FontAwesome.Solid.ExclamationTriangle, - Text = EditorStrings.TooLongTimestamp - })); - return; - } - - editor.SeekAndSelectHitObjects(timeGroup, objectsGroup); + Icon = FontAwesome.Solid.ExclamationTriangle, + Text = message + })); } /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 80e01d4eb7..60d26d9ec0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1138,11 +1138,33 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } - public void SeekAndSelectHitObjects(string timeGroup, string objectsGroup) + public void SeekAndSelectHitObjects(string timestamp, Action onError) { + string[] groups = EditorTimestampParser.GetRegexGroups(timestamp); + + if (groups.Length != 2 || string.IsNullOrEmpty(groups[0])) + { + onError.Invoke(EditorStrings.FailedToProcessTimestamp); + return; + } + + string timeGroup = groups[0]; + string objectsGroup = groups[1]; + string timeMinutes = timeGroup.Split(':').FirstOrDefault() ?? string.Empty; + + // Currently, lazer chat highlights infinite-long editor links like `10000000000:00:000 (1)` + // Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues + if (timeMinutes.Length > 5 || double.Parse(timeMinutes) > 30_000) + { + onError.Invoke(EditorStrings.TooLongTimestamp); + return; + } + double position = EditorTimestampParser.GetTotalMilliseconds(timeGroup); + editorBeatmap.SelectedHitObjects.Clear(); + // Only seeking is necessary if (string.IsNullOrEmpty(objectsGroup)) { if (clock.IsRunning) @@ -1155,8 +1177,9 @@ namespace osu.Game.Screens.Edit if (Mode.Value != EditorScreenMode.Compose) Mode.Value = EditorScreenMode.Compose; - // Seek to the next closest HitObject's position + // Seek to the next closest HitObject HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position); + if (nextObject != null && nextObject.StartTime > 0) position = nextObject.StartTime; diff --git a/osu.Game/Screens/Edit/EditorTimestampParser.cs b/osu.Game/Screens/Edit/EditorTimestampParser.cs index 44d614ca70..2d8f8a8f4c 100644 --- a/osu.Game/Screens/Edit/EditorTimestampParser.cs +++ b/osu.Game/Screens/Edit/EditorTimestampParser.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit return (times[0] * 60 + times[1]) * 1_000 + times[2]; } - public static List GetSelectedHitObjects(IEnumerable editorHitObjects, string objectsGroup, double position) + public static List GetSelectedHitObjects(IReadOnlyList editorHitObjects, string objectsGroup, double position) { List hitObjects = editorHitObjects.Where(x => x.StartTime >= position).ToList(); List selectedObjects = new List(); @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit } // Stable behavior - // - always selects next closest object when `objectsGroup` only has one, non-Column item + // - always selects the next closest object when `objectsGroup` only has one (combo) item if (objectsToSelect.Length != 1 || objectsGroup.Contains('|')) return selectedObjects; From 91cf237fc12fcbecc6d1345310717407f559a32e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 4 Nov 2023 02:46:28 +0300 Subject: [PATCH 509/896] Revert health display settings changes --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index cc69acf8d6..1a213ddc6f 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } [SettingSource("Bar height")] - public BindableFloat BarHeight { get; } = new BindableFloat(30) + public BindableFloat BarHeight { get; } = new BindableFloat(20) { MinValue = 0, MaxValue = 64, @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Play.HUD }; [SettingSource("Bar length")] - public BindableFloat BarLength { get; } = new BindableFloat(0.35f) + public BindableFloat BarLength { get; } = new BindableFloat(0.98f) { MinValue = 0.2f, MaxValue = 1, From 1c844a155e97059e23cdae6470401d217fa88db3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 01:52:12 +0300 Subject: [PATCH 510/896] Remove health line detail --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 64 ++++++++----------- osu.Game/Skinning/ArgonSkin.cs | 4 +- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 1a213ddc6f..793d43f7ef 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; -using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; @@ -101,45 +100,36 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChildren = new[] + InternalChild = new Container { - new Circle + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Position = new Vector2(-4f, main_path_radius - 1.5f), - Size = new Vector2(50f, 3f), - }, - new Container - { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Left = 50f }, - Children = new Drawable[] + background = new BackgroundPath { - background = new BackgroundPath - { - PathRadius = main_path_radius, - }, - glowBar = new BarPath - { - BarColour = Color4.White, - GlowColour = main_bar_glow_colour, - Blending = BlendingParameters.Additive, - Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), - PathRadius = 40f, - // Kinda hacky, but results in correct positioning with increased path radius. - Margin = new MarginPadding(-30f), - GlowPortion = 0.9f, - }, - mainBar = new BarPath - { - AutoSizeAxes = Axes.None, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - BarColour = main_bar_colour, - GlowColour = main_bar_glow_colour, - PathRadius = main_path_radius, - GlowPortion = 0.6f, - }, - } + PathRadius = main_path_radius, + }, + glowBar = new BarPath + { + BarColour = Color4.White, + GlowColour = main_bar_glow_colour, + Blending = BlendingParameters.Additive, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White), + PathRadius = 40f, + // Kinda hacky, but results in correct positioning with increased path radius. + Margin = new MarginPadding(-30f), + GlowPortion = 0.9f, + }, + mainBar = new BarPath + { + AutoSizeAxes = Axes.None, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + BarColour = main_bar_colour, + GlowColour = main_bar_glow_colour, + PathRadius = main_path_radius, + GlowPortion = 0.6f, + }, } }; } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 07d9afffac..f850ae9cbd 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -130,8 +130,8 @@ namespace osu.Game.Skinning // elements default to beneath the health bar const float components_x_offset = 50; - health.Anchor = Anchor.TopLeft; - health.Origin = Anchor.TopLeft; + health.Anchor = Anchor.TopCentre; + health.Origin = Anchor.TopCentre; health.Y = 15; if (scoreWedge != null) From 58a830fc5b2bd749ebccd7cf1b5e0f12b6f4c117 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 01:55:12 +0300 Subject: [PATCH 511/896] Revert "Add "Argon" performance points counter" This reverts commit 56eeb117ce2011ad4b80cf32c3b015eb482d7c9b. --- .../TestScenePerformancePointsCounter.cs | 5 +- .../Play/ArgonPerformancePointsCounter.cs | 62 ------------------- 2 files changed, 3 insertions(+), 64 deletions(-) delete mode 100644 osu.Game/Screens/Play/ArgonPerformancePointsCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs index 9b8346cf12..9622caabf5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osuTK; namespace osu.Game.Tests.Visual.Gameplay @@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Gameplay private int iteration; private Bindable lastJudgementResult = new Bindable(); - private ArgonPerformancePointsCounter counter; + private PerformancePointsCounter counter; [SetUpSteps] public void SetUpSteps() => AddStep("create components", () => @@ -65,7 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void createCounter() => AddStep("Create counter", () => { - dependencyContainer.Child = counter = new ArgonPerformancePointsCounter + dependencyContainer.Child = counter = new PerformancePointsCounter { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Play/ArgonPerformancePointsCounter.cs b/osu.Game/Screens/Play/ArgonPerformancePointsCounter.cs deleted file mode 100644 index f0e0e1f0a4..0000000000 --- a/osu.Game/Screens/Play/ArgonPerformancePointsCounter.cs +++ /dev/null @@ -1,62 +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.Extensions.LocalisationExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Resources.Localisation.Web; -using osu.Game.Screens.Play.HUD; -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play -{ - public partial class ArgonPerformancePointsCounter : GameplayPerformancePointsCounter, ISerialisableDrawable - { - public bool UsesFixedAnchor { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - } - - protected override IHasText CreateText() => new TextComponent(); - - private partial class TextComponent : CompositeDrawable, IHasText - { - private readonly OsuSpriteText text; - - public LocalisableString Text - { - get => text.Text; - set => text.Text = value; - } - - public TextComponent() - { - AutoSizeAxes = Axes.Both; - - InternalChild = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Children = new[] - { - new OsuSpriteText - { - Text = BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), - }, - text = new OsuSpriteText - { - Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.Regular), - } - } - }; - } - } - } -} From a23dfbeab55bb3e195407bcf436444f150d5a355 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 01:55:13 +0300 Subject: [PATCH 512/896] Revert "Abstractify PP counter logic from the "Triangles" implementation" This reverts commit daf4a03fd092ac84955a8f63fa3c45200a28cf04. --- .../HUD/GameplayPerformancePointsCounter.cs | 186 ------------------ .../Play/HUD/PerformancePointsCounter.cs | 181 ++++++++++++++++- 2 files changed, 175 insertions(+), 192 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/GameplayPerformancePointsCounter.cs diff --git a/osu.Game/Screens/Play/HUD/GameplayPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/GameplayPerformancePointsCounter.cs deleted file mode 100644 index a812a3e043..0000000000 --- a/osu.Game/Screens/Play/HUD/GameplayPerformancePointsCounter.cs +++ /dev/null @@ -1,186 +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; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Audio.Track; -using osu.Framework.Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Textures; -using osu.Framework.Localisation; -using osu.Game.Beatmaps; -using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - public abstract partial class GameplayPerformancePointsCounter : RollingCounter - { - protected override bool IsRollingProportional => true; - - protected override double RollingDuration => 1000; - - private const float alpha_when_invalid = 0.3f; - - [Resolved] - private ScoreProcessor scoreProcessor { get; set; } - - [Resolved] - private GameplayState gameplayState { get; set; } - - [CanBeNull] - private List timedAttributes; - - private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); - - private JudgementResult lastJudgement; - private PerformanceCalculator performanceCalculator; - private ScoreInfo scoreInfo; - - protected GameplayPerformancePointsCounter() - { - Current.Value = DisplayedCount = 0; - } - - private Mod[] clonedMods; - - [BackgroundDependencyLoader] - private void load(BeatmapDifficultyCache difficultyCache) - { - DrawableCount.Alpha = alpha_when_invalid; - - if (gameplayState != null) - { - performanceCalculator = gameplayState.Ruleset.CreatePerformanceCalculator(); - clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray(); - - scoreInfo = new ScoreInfo(gameplayState.Score.ScoreInfo.BeatmapInfo, gameplayState.Score.ScoreInfo.Ruleset) { Mods = clonedMods }; - - var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); - difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token) - .ContinueWith(task => Schedule(() => - { - timedAttributes = task.GetResultSafely(); - - IsValid = true; - - if (lastJudgement != null) - onJudgementChanged(lastJudgement); - }), TaskContinuationOptions.OnlyOnRanToCompletion); - } - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (scoreProcessor != null) - { - scoreProcessor.NewJudgement += onJudgementChanged; - scoreProcessor.JudgementReverted += onJudgementChanged; - } - - if (gameplayState?.LastJudgementResult.Value != null) - onJudgementChanged(gameplayState.LastJudgementResult.Value); - } - - private bool isValid; - - protected bool IsValid - { - set - { - if (value == isValid) - return; - - isValid = value; - DrawableCount.FadeTo(isValid ? 1 : alpha_when_invalid, 1000, Easing.OutQuint); - } - } - - private void onJudgementChanged(JudgementResult judgement) - { - lastJudgement = judgement; - - var attrib = getAttributeAtTime(judgement); - - if (gameplayState == null || attrib == null || scoreProcessor == null) - { - IsValid = false; - return; - } - - scoreProcessor.PopulateScore(scoreInfo); - Current.Value = (int)Math.Round(performanceCalculator?.Calculate(scoreInfo, attrib).Total ?? 0, MidpointRounding.AwayFromZero); - IsValid = true; - } - - [CanBeNull] - private DifficultyAttributes getAttributeAtTime(JudgementResult judgement) - { - if (timedAttributes == null || timedAttributes.Count == 0) - return null; - - int attribIndex = timedAttributes.BinarySearch(new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); - if (attribIndex < 0) - attribIndex = ~attribIndex - 1; - - return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes; - } - - protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (scoreProcessor != null) - { - scoreProcessor.NewJudgement -= onJudgementChanged; - scoreProcessor.JudgementReverted -= onJudgementChanged; - } - - loadCancellationSource?.Cancel(); - } - - // TODO: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. - private class GameplayWorkingBeatmap : WorkingBeatmap - { - private readonly IBeatmap gameplayBeatmap; - - public GameplayWorkingBeatmap(IBeatmap gameplayBeatmap) - : base(gameplayBeatmap.BeatmapInfo, null) - { - this.gameplayBeatmap = gameplayBeatmap; - } - - public override IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList mods, CancellationToken cancellationToken) - => gameplayBeatmap; - - protected override IBeatmap GetBeatmap() => gameplayBeatmap; - - public override Texture GetBackground() => throw new NotImplementedException(); - - protected override Track GetBeatmapTrack() => throw new NotImplementedException(); - - protected internal override ISkin GetSkin() => throw new NotImplementedException(); - - public override Stream GetStream(string storagePath) => throw new NotImplementedException(); - } - } -} diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 76feae88af..82f116b4ae 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -1,31 +1,175 @@ // 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.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Play.HUD { - // todo: this should be renamed to DefaultPerformancePointsCounter, or probably even TrianglesPerformancePointsCounter once all other triangles components are renamed accordingly. - public partial class PerformancePointsCounter : GameplayPerformancePointsCounter, ISerialisableDrawable + public partial class PerformancePointsCounter : RollingCounter, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + protected override bool IsRollingProportional => true; + + protected override double RollingDuration => 1000; + + private const float alpha_when_invalid = 0.3f; + + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } + + [Resolved] + private GameplayState gameplayState { get; set; } + + [CanBeNull] + private List timedAttributes; + + private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); + + private JudgementResult lastJudgement; + private PerformanceCalculator performanceCalculator; + private ScoreInfo scoreInfo; + + public PerformancePointsCounter() { - Colour = colours.BlueLighter; + Current.Value = DisplayedCount = 0; } - protected override IHasText CreateText() => new TextComponent(); + private Mod[] clonedMods; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache) + { + Colour = colours.BlueLighter; + + if (gameplayState != null) + { + performanceCalculator = gameplayState.Ruleset.CreatePerformanceCalculator(); + clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray(); + + scoreInfo = new ScoreInfo(gameplayState.Score.ScoreInfo.BeatmapInfo, gameplayState.Score.ScoreInfo.Ruleset) { Mods = clonedMods }; + + var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); + difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token) + .ContinueWith(task => Schedule(() => + { + timedAttributes = task.GetResultSafely(); + + IsValid = true; + + if (lastJudgement != null) + onJudgementChanged(lastJudgement); + }), TaskContinuationOptions.OnlyOnRanToCompletion); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (scoreProcessor != null) + { + scoreProcessor.NewJudgement += onJudgementChanged; + scoreProcessor.JudgementReverted += onJudgementChanged; + } + + if (gameplayState?.LastJudgementResult.Value != null) + onJudgementChanged(gameplayState.LastJudgementResult.Value); + } + + private bool isValid; + + protected bool IsValid + { + set + { + if (value == isValid) + return; + + isValid = value; + DrawableCount.FadeTo(isValid ? 1 : alpha_when_invalid, 1000, Easing.OutQuint); + } + } + + private void onJudgementChanged(JudgementResult judgement) + { + lastJudgement = judgement; + + var attrib = getAttributeAtTime(judgement); + + if (gameplayState == null || attrib == null || scoreProcessor == null) + { + IsValid = false; + return; + } + + scoreProcessor.PopulateScore(scoreInfo); + Current.Value = (int)Math.Round(performanceCalculator?.Calculate(scoreInfo, attrib).Total ?? 0, MidpointRounding.AwayFromZero); + IsValid = true; + } + + [CanBeNull] + private DifficultyAttributes getAttributeAtTime(JudgementResult judgement) + { + if (timedAttributes == null || timedAttributes.Count == 0) + return null; + + int attribIndex = timedAttributes.BinarySearch(new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + if (attribIndex < 0) + attribIndex = ~attribIndex - 1; + + return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes; + } + + protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); + + protected override IHasText CreateText() => new TextComponent + { + Alpha = alpha_when_invalid + }; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (scoreProcessor != null) + { + scoreProcessor.NewJudgement -= onJudgementChanged; + scoreProcessor.JudgementReverted -= onJudgementChanged; + } + + loadCancellationSource?.Cancel(); + } private partial class TextComponent : CompositeDrawable, IHasText { @@ -65,5 +209,30 @@ namespace osu.Game.Screens.Play.HUD }; } } + + // TODO: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. + private class GameplayWorkingBeatmap : WorkingBeatmap + { + private readonly IBeatmap gameplayBeatmap; + + public GameplayWorkingBeatmap(IBeatmap gameplayBeatmap) + : base(gameplayBeatmap.BeatmapInfo, null) + { + this.gameplayBeatmap = gameplayBeatmap; + } + + public override IBeatmap GetPlayableBeatmap(IRulesetInfo ruleset, IReadOnlyList mods, CancellationToken cancellationToken) + => gameplayBeatmap; + + protected override IBeatmap GetBeatmap() => gameplayBeatmap; + + public override Texture GetBackground() => throw new NotImplementedException(); + + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); + + protected internal override ISkin GetSkin() => throw new NotImplementedException(); + + public override Stream GetStream(string storagePath) => throw new NotImplementedException(); + } } } From 6c3169a0ed6e9f0f9e1e7d668993e04861f63540 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 01:56:45 +0300 Subject: [PATCH 513/896] Remove PP wedge and logic for gameplay layout --- osu.Game/Screens/Play/HUD/ArgonRightWedge.cs | 29 -------------------- osu.Game/Skinning/ArgonSkin.cs | 18 ------------ 2 files changed, 47 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/ArgonRightWedge.cs diff --git a/osu.Game/Screens/Play/HUD/ArgonRightWedge.cs b/osu.Game/Screens/Play/HUD/ArgonRightWedge.cs deleted file mode 100644 index 6d01fecf13..0000000000 --- a/osu.Game/Screens/Play/HUD/ArgonRightWedge.cs +++ /dev/null @@ -1,29 +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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - public partial class ArgonRightWedge : CompositeDrawable, ISerialisableDrawable - { - public bool UsesFixedAnchor { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - AutoSizeAxes = Axes.Both; - - InternalChild = new ArgonWedgePiece - { - WedgeWidth = { Value = 274 }, - WedgeHeight = { Value = 40 }, - InvertShear = { Value = true }, - EndOpacity = 0.5f, - }; - } - } -} diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index f850ae9cbd..3262812d24 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -120,8 +120,6 @@ namespace osu.Game.Skinning var accuracy = container.OfType().FirstOrDefault(); var comboWedge = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); - var rightWedge = container.OfType().FirstOrDefault(); - var ppCounter = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); @@ -162,20 +160,6 @@ namespace osu.Game.Skinning } } - if (rightWedge != null) - { - rightWedge.Anchor = Anchor.TopRight; - rightWedge.Origin = Anchor.TopRight; - rightWedge.Position = new Vector2(180, 20); - - if (ppCounter != null) - { - ppCounter.Anchor = Anchor.TopRight; - ppCounter.Origin = Anchor.TopRight; - ppCounter.Position = new Vector2(rightWedge.X - 240, rightWedge.Y + 8); - } - } - var hitError = container.OfType().FirstOrDefault(); if (hitError != null) @@ -222,8 +206,6 @@ namespace osu.Game.Skinning new ArgonAccuracyCounter(), new ArgonComboWedge(), new ArgonComboCounter(), - new ArgonRightWedge(), - new ArgonPerformancePointsCounter(), new BarHitErrorMeter(), new BarHitErrorMeter(), new ArgonSongProgress(), From 77f5a4cdf53fca660a71140b84c9897439ddbf2e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 02:01:26 +0300 Subject: [PATCH 514/896] Update skin deserialisation archive --- .../Archives/modified-argon-pro-20231026.osk | Bin 2066 -> 0 bytes .../Archives/modified-argon-pro-20231105.osk | Bin 0 -> 1899 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-pro-20231026.osk create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-pro-20231105.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231026.osk b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231026.osk deleted file mode 100644 index e349c14d7403a13b92ae0b5fed2ee449273a199b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2066 zcmZ`)2T&7e77m0Enlyuiw!j8OTtozAAuN$1H30&F&;*VWFo7g=G@x`?nj)@LF(5@o ziUv%GA}CGiaIm3BmEKvJQe?fX=ba;W`~UOipZCvvZ@%}aF``j#>Tx_9IEfXkDYGelFnDBX>BKD|+L0&LpEeSmEZuGOG>$7d&!V{S7tZ7%*`+j4*zLTXPoRVl)r{c&!KkoM3Y^cf*rR zY%d#-iGE}fjuaI5h2&}ul02d*9J#(H7T=>GzocizKa+N9)r)qTaN&k@l79YdV)HR` zZiv|6>CK7Ebmu`KhDo$hfb$60KpkSZnJXdqcAwI>ymRhCE{zr$F+*Sa%sL?8usQq} zy&7F22$f;T>xE%IzXFvlmR0FTR4e6|p@6NTsa&~*9UsLPQ1fOkoX1O;ld~0KLX-(d zA&RVS=9M;#(8US8^_QY0eST*O)N&H_$Tq2A=GPfJ$Pod<2ksyhGJ zYgD{Dcf96(wzmTopV8%{C<8|;Bp`Fn4r=8oYrLYIBL1Oc*H0s8ZMsj;cN@-cBjSf$ zVqI3=8ebKP`M|JXz6-t}U7NKhbKoG%%p#i9#t#6bqUy1fc290tOR>_(2d2FL)bh~u zO<*{^$2nlj+Nanx#t{2Z97Q&P>o|q1L=FXtjHV%UZtp#*5+U*R6-6UW%p0nucyVJS z?ZS@V(Dy8sf%7uw_Okd%OT19$2BW4bbal|OeqQW5hyoV7}Z^JrM!F{CTcA$^BeUCNBWXW6a9r(;SZ^?rv-JWKgevj`9I?= zfC)O}9>^tHLBU+>n@x#h+@`vRha=qQZ>YW65$Rk4S$c=}y)+b~j1*igaIK4t3zhV{ z4t`gJ}2RdBRg>vakF9U(|Hg=;)PthU1jv7&X;C=Q;G?hnLXJ0TVwBKM2t`i#TO zS&VPiEiVgXj1>R*iT&Cp^Dtu;D#!JE6{YXFQ%i&Yg+>?y5696=lgA)Sy`+dd^`|lI=*Ylt4 zb`*7VLwv;m08ukvQUBc+N1;at_%~=IyFmW`I6oTWXgPh0p#l1z;yMaGs^T|rIQLOa T(MS;c4*&qMuL_%9_ZRP9@fu;D diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231105.osk b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231105.osk new file mode 100644 index 0000000000000000000000000000000000000000..16f89502eff0bc46df9922fec370ee1d804170f6 GIT binary patch literal 1899 zcmWIWW@Zs#U|`^2U=>IWU(2a$c^JquWCDpWFcfEJ=ILeTWuEQdo5y4*&{A!Gw1sPl z%y(f;r;?D8l}@c1Vtd%dzP&s7{PN3ry-NL4!aF>tXnFaYU#gL+{apE5!+TG>#M-L$ zizgfVX?3b5E|D`);hl1)bI*K-YnzT1HhnuR{6^sY4(_?fzVs-6-5KKe+J>9etx|rg zjG)EDr7qr^?{6wlsW-@<8S$Uztj4_1v=@t#?uGbnK47z=bZzo(gV~C)ChIm%Uj0Jj zn9UA@Z~Hg?IM(GODp|EM?)BA&2jBf!92Fiq*LK&-{&(U5C>}LoaOf%ndaxUad7&Q7 z%uCDH%PP*#Gwm_tI^-bW`g=~&Osm~FN$KihlhimKF^X;GbUrcPAW~59{krBG`~Lr{ z4m}q*f0|FVdpD0wkzCCs&j;b6_m7obKXPNo>^1(Y&e~=9xnAK5KQn>7ZqMN`A+F_z zn)nvaP`qtBXW!3)nUV=U4-?*Q?rOaCw|vz-c~l36?qoWA5f}^S6c`x9fDZIc%*^u$ zb#czmEy&MH%_}JeyY%hM`}wyF1diYT9qwY|?9y<_sI$E<@3o1qbM|iAkkBVHvL9W& z@#2lc;#L2D&vDQU@RyFAtT5|C!>7I7^|j~Ht#{fN%(m=?xLsCSyCL!Zy#JO{IqVZfoDx4sx10T08KL%XUT@v|qaQxInN8Sup`q1r zv(f8mHmUJ5Z)W{9ZYyX`i>b9#4B4#m+RJ+OjJ=PQ(w=cHH58g9;b^v#aV$GrthTgy>^{vX^<~pp_~0Hd#~SvR7WADnJ(QoPE5=_ zcC{e#jaOIQ~!qITd~rQOiRVTX)KldmQ%dB_Rh`6=7me;zH9BN zd~kQYbJfu|_A}p}Q}6wIc-yU}yZ@F2F1@wq=k)(wds_O#^O=kL&KSiwvu#P5_V|hq z`_1_q1E zzyL``1_onbM&Ji#g5dnT^x)K-)Z`Ly>d&3%pMBVX=ji*tBFCO5gqdwKdXbn?FJZlA z&0W1zcJ)x<;83n@hoijq{eCkcHIvhB#`nozzfABKR(!Jfs=bwS#ZukV)rDL-7nm$B zOMKaNb From 634795e45f12676c277be583bad61c29cc3ec22a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 02:50:14 +0300 Subject: [PATCH 515/896] Adjust failing test scenes --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 4e5db5d46e..ec7d92faac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Begin drag top left", () => { - InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4)); + InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4, box1.ScreenSpaceDrawQuad.Height / 8)); InputManager.PressButton(MouseButton.Left); }); @@ -146,8 +146,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("Add big black box", () => { - InputManager.MoveMouseTo(skinEditor.ChildrenOfType().First()); - InputManager.Click(MouseButton.Left); + skinEditor.ChildrenOfType().First(b => b.ChildrenOfType().FirstOrDefault() != null).TriggerClick(); }); AddStep("store box", () => From 48a75f6152705c5377b116f0dc2b6cf598036bb3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 06:28:10 +0300 Subject: [PATCH 516/896] Fix resume cursor following gameplay cursor scale setting --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 555610a3b6..bcfefe856d 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -14,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI @@ -25,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.UI private OsuClickToResumeCursor clickToResumeCursor; private OsuCursorContainer localCursorContainer; - private IBindable localCursorScale; public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; @@ -49,13 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI clickToResumeCursor.Appear(); if (localCursorContainer == null) - { Add(localCursorContainer = new OsuCursorContainer()); - - localCursorScale = new BindableFloat(); - localCursorScale.BindTo(localCursorContainer.CursorScale); - localCursorScale.BindValueChanged(scale => cursorScaleContainer.Scale = new Vector2(scale.NewValue), true); - } } protected override void PopOut() From 99405a2bbd38e999672d0fe89c83e56b845a0bdf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 06:28:34 +0300 Subject: [PATCH 517/896] Tidy up resume overlay test scene --- .../TestSceneResumeOverlay.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 0bb27cff0f..81dc64cda9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.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 NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -12,14 +13,15 @@ namespace osu.Game.Rulesets.Osu.Tests { public partial class TestSceneResumeOverlay : OsuManualInputManagerTestScene { - public TestSceneResumeOverlay() + private ManualOsuInputManager osuInputManager = null!; + private CursorContainer cursor = null!; + private ResumeOverlay resume = null!; + + private bool resumeFired; + + [SetUp] + public void SetUp() => Schedule(() => { - ManualOsuInputManager osuInputManager; - CursorContainer cursor; - ResumeOverlay resume; - - bool resumeFired = false; - Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) { Children = new Drawable[] @@ -32,8 +34,13 @@ namespace osu.Game.Rulesets.Osu.Tests } }; + resumeFired = false; resume.ResumeAction = () => resumeFired = true; + }); + [Test] + public void TestResume() + { AddStep("move mouse to center", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre)); AddStep("show", () => resume.Show()); From 9cb331641c39b9a1b54b192be3420e0db8b7ba68 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 5 Nov 2023 06:34:09 +0300 Subject: [PATCH 518/896] Rename container --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index bcfefe856d..12506c83b9 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.UI { public partial class OsuResumeOverlay : ResumeOverlay { - private Container cursorScaleContainer; + private Container resumeCursorContainer; private OsuClickToResumeCursor clickToResumeCursor; private OsuCursorContainer localCursorContainer; @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load() { - Add(cursorScaleContainer = new Container + Add(resumeCursorContainer = new Container { Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume } }); @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI base.PopIn(); GameplayCursor.ActiveCursor.Hide(); - cursorScaleContainer.Position = ToLocalSpace(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre); + resumeCursorContainer.Position = ToLocalSpace(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre); clickToResumeCursor.Appear(); if (localCursorContainer == null) From 86fb33cb90b500dba57b2c199b381e7e8473b558 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 5 Nov 2023 12:40:06 +0100 Subject: [PATCH 519/896] Add disable taps checkbox to touch input settings --- osu.Android/OsuGameAndroid.cs | 4 +++ osu.Game/Configuration/OsuConfigManager.cs | 4 +++ osu.Game/Localisation/TouchSettingsStrings.cs | 24 ++++++++++++++ .../Settings/Sections/Input/TouchSettings.cs | 32 ++++++++++++------- 4 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Localisation/TouchSettingsStrings.cs diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index dea70e6b27..e4b934a387 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers; using osu.Framework.Platform; using osu.Game; using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Updater; using osu.Game.Utils; @@ -97,6 +98,9 @@ namespace osu.Android case AndroidJoystickHandler jh: return new AndroidJoystickSettings(jh); + case AndroidTouchHandler: + return new TouchSettings(handler); + default: return base.CreateSettingsSubsectionFor(handler); } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 5d2d782063..339817985e 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -330,6 +330,10 @@ namespace osu.Game.Configuration ShowHealthDisplayWhenCantFail, FadePlayfieldWhenHealthLow, + + /// + /// Disables mouse buttons clicks and touchscreen taps during gameplay. + /// MouseDisableButtons, MouseDisableWheel, ConfineMouseMode, diff --git a/osu.Game/Localisation/TouchSettingsStrings.cs b/osu.Game/Localisation/TouchSettingsStrings.cs new file mode 100644 index 0000000000..785b333100 --- /dev/null +++ b/osu.Game/Localisation/TouchSettingsStrings.cs @@ -0,0 +1,24 @@ +// 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.Localisation; + +namespace osu.Game.Localisation +{ + public static class TouchSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.TouchSettings"; + + /// + /// "Touch" + /// + public static LocalisableString Touch => new TranslatableString(getKey(@"touch"), @"Touch"); + + /// + /// "Disable taps during gameplay" + /// + public static LocalisableString DisableTapsDuringGameplay => new TranslatableString(getKey(@"disable_taps_during_gameplay"), @"Disable taps during gameplay"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs index 8d1b12d5b2..b1b1b59429 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs @@ -3,38 +3,48 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Input.Handlers.Touch; +using osu.Framework.Input.Handlers; using osu.Framework.Localisation; +using osu.Game.Configuration; using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Input { + /// + /// Touch input settings subsection common to all touch handlers (even on different platforms). + /// public partial class TouchSettings : SettingsSubsection { - private readonly TouchHandler handler; + private readonly InputHandler handler; - public TouchSettings(TouchHandler handler) + protected override LocalisableString Header => TouchSettingsStrings.Touch; + + public TouchSettings(InputHandler handler) { this.handler = handler; } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager osuConfig) { - Children = new Drawable[] + if (!RuntimeInfo.IsMobile) // don't allow disabling the only input method (touch) on mobile. { - new SettingsCheckbox + Add(new SettingsCheckbox { LabelText = CommonStrings.Enabled, Current = handler.Enabled - }, - }; + }); + } + + Add(new SettingsCheckbox + { + LabelText = TouchSettingsStrings.DisableTapsDuringGameplay, + Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) + }); } public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"touchscreen" }); - - protected override LocalisableString Header => handler.Description; } } From fa1d1df594d3f5307e01207952c4eb6be7ee3494 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 5 Nov 2023 12:43:14 +0100 Subject: [PATCH 520/896] Rename mouse button string to `Disable clicks during gameplay` --- osu.Android/AndroidMouseSettings.cs | 2 +- osu.Game/Localisation/MouseSettingsStrings.cs | 6 +++--- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 2 +- osu.Game/Screens/Play/PlayerSettings/InputSettings.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Android/AndroidMouseSettings.cs b/osu.Android/AndroidMouseSettings.cs index d6d7750448..fd01b11164 100644 --- a/osu.Android/AndroidMouseSettings.cs +++ b/osu.Android/AndroidMouseSettings.cs @@ -70,7 +70,7 @@ namespace osu.Android }, new SettingsCheckbox { - LabelText = MouseSettingsStrings.DisableMouseButtons, + LabelText = MouseSettingsStrings.DisableClicksDuringGameplay, Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons), }, }); diff --git a/osu.Game/Localisation/MouseSettingsStrings.cs b/osu.Game/Localisation/MouseSettingsStrings.cs index 1772f03b29..e61af07364 100644 --- a/osu.Game/Localisation/MouseSettingsStrings.cs +++ b/osu.Game/Localisation/MouseSettingsStrings.cs @@ -40,14 +40,14 @@ namespace osu.Game.Localisation public static LocalisableString DisableMouseWheelVolumeAdjust => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust"), @"Disable mouse wheel adjusting volume during gameplay"); /// - /// "Volume can still be adjusted using the mouse wheel by holding "Alt"" + /// "Volume can still be adjusted using the mouse wheel by holding "Alt"" /// public static LocalisableString DisableMouseWheelVolumeAdjustTooltip => new TranslatableString(getKey(@"disable_mouse_wheel_volume_adjust_tooltip"), @"Volume can still be adjusted using the mouse wheel by holding ""Alt"""); /// - /// "Disable mouse buttons during gameplay" + /// "Disable clicks during gameplay" /// - public static LocalisableString DisableMouseButtons => new TranslatableString(getKey(@"disable_mouse_buttons"), @"Disable mouse buttons during gameplay"); + public static LocalisableString DisableClicksDuringGameplay => new TranslatableString(getKey(@"disable_clicks"), @"Disable clicks during gameplay"); /// /// "Enable high precision mouse to adjust sensitivity" diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index dfaeafbf5d..6bf06f4f98 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, new SettingsCheckbox { - LabelText = MouseSettingsStrings.DisableMouseButtons, + LabelText = MouseSettingsStrings.DisableClicksDuringGameplay, Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) }, }; diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index cf261ba49b..4076782ee1 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { mouseButtonsCheckbox = new PlayerCheckbox { - LabelText = MouseSettingsStrings.DisableMouseButtons + LabelText = MouseSettingsStrings.DisableClicksDuringGameplay } }; } From 0d8bfedf5d3693809e76471b9feb47ebf140e40b Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 5 Nov 2023 12:44:22 +0100 Subject: [PATCH 521/896] Rename popup/binding string to `Toggle gameplay clicks/taps` --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 339817985e..e3f950ce2c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -246,7 +246,7 @@ namespace osu.Game.Configuration ), new TrackedSetting(OsuSetting.MouseDisableButtons, disabledState => new SettingDescription( rawValue: !disabledState, - name: GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons, + name: GlobalActionKeyBindingStrings.ToggleGameplayClicksTaps, value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(), shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons)) ), diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 947cd5f54f..b8163cc3b1 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -237,7 +237,7 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.TakeScreenshot))] TakeScreenshot, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons))] + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleGameplayClicksTaps))] ToggleGameplayMouseButtons, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.Back))] diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 8356c480dd..1bbbbdc3bc 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -70,9 +70,9 @@ namespace osu.Game.Localisation public static LocalisableString TakeScreenshot => new TranslatableString(getKey(@"take_screenshot"), @"Take screenshot"); /// - /// "Toggle gameplay mouse buttons" + /// "Toggle gameplay clicks/taps" /// - public static LocalisableString ToggleGameplayMouseButtons => new TranslatableString(getKey(@"toggle_gameplay_mouse_buttons"), @"Toggle gameplay mouse buttons"); + public static LocalisableString ToggleGameplayClicksTaps => new TranslatableString(getKey(@"toggle_gameplay_clicks_taps"), @"Toggle gameplay clicks/taps"); /// /// "Back" From 9947897c5f83c7298c381015a95673902486b52d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 5 Nov 2023 12:53:40 +0100 Subject: [PATCH 522/896] Use appropriate clicks/taps text in player loader input settings --- osu.Game/Screens/Play/PlayerSettings/InputSettings.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 4076782ee1..f6b0cddcf1 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.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; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; @@ -19,7 +20,8 @@ namespace osu.Game.Screens.Play.PlayerSettings { mouseButtonsCheckbox = new PlayerCheckbox { - LabelText = MouseSettingsStrings.DisableClicksDuringGameplay + // TODO: change to touchscreen detection once https://github.com/ppy/osu/pull/25348 makes it in + LabelText = RuntimeInfo.IsDesktop ? MouseSettingsStrings.DisableClicksDuringGameplay : TouchSettingsStrings.DisableTapsDuringGameplay } }; } From 277cf7dc127cfacaf62f51bc88e0a0a3c1474382 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sun, 5 Nov 2023 18:26:51 +0100 Subject: [PATCH 523/896] Ensure every SelectedItem is alive and has Blueprint --- .../Components/EditorBlueprintContainer.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index ad0e8b124b..60959ca27a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -51,6 +52,10 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded += AddBlueprintFor; Beatmap.HitObjectRemoved += RemoveBlueprintFor; + // This makes sure HitObjects will have active Blueprints ready to display + // after clicking on an Editor Timestamp/Link + Beatmap.SelectedHitObjects.CollectionChanged += SetHitObjectsAlive; + if (Composer != null) { foreach (var obj in Composer.HitObjects) @@ -144,6 +149,15 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray()); } + protected void SetHitObjectsAlive(object sender, NotifyCollectionChangedEventArgs e) + { + if (e == null || e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null) + return; + + foreach (HitObject item in e.NewItems) + Composer.Playfield.SetKeepAlive(item, true); + } + protected override void OnBlueprintSelected(SelectionBlueprint blueprint) { base.OnBlueprintSelected(blueprint); @@ -166,6 +180,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Beatmap.HitObjectAdded -= AddBlueprintFor; Beatmap.HitObjectRemoved -= RemoveBlueprintFor; + Beatmap.SelectedHitObjects.CollectionChanged -= SetHitObjectsAlive; } usageEventBuffer?.Dispose(); From e2b07628fb5719540ea3d1405de25a83b469b8da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 15:24:12 +0900 Subject: [PATCH 524/896] Add player name skin component 3 minute implementation. Addresses https://github.com/ppy/osu/discussions/25340. --- osu.Game/Skinning/Components/PlayerName.cs | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 osu.Game/Skinning/Components/PlayerName.cs diff --git a/osu.Game/Skinning/Components/PlayerName.cs b/osu.Game/Skinning/Components/PlayerName.cs new file mode 100644 index 0000000000..34ace53d47 --- /dev/null +++ b/osu.Game/Skinning/Components/PlayerName.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Play; + +namespace osu.Game.Skinning.Components +{ + [UsedImplicitly] + public partial class PlayerName : FontAdjustableSkinComponent + { + private readonly OsuSpriteText text; + + public PlayerName() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + }; + } + + [BackgroundDependencyLoader] + private void load(GameplayState gameplayState) + { + text.Text = gameplayState.Score.ScoreInfo.User.Username; + } + + protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); + } +} From 11bd801795f5a838cfc478e486586476fed2eb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 07:44:25 +0100 Subject: [PATCH 525/896] Use more intelligent default for `TouchInputActive` --- osu.Game/Configuration/SessionStatics.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 0fc2076a7e..8f0a60b23d 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -3,6 +3,7 @@ #nullable disable +using osu.Framework; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API.Requests.Responses; @@ -25,7 +26,7 @@ namespace osu.Game.Configuration SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); - SetDefault(Static.TouchInputActive, false); + SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); } /// From 3c72c5bccd21b4127ceaae9346ec04e6b059db9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 07:48:09 +0100 Subject: [PATCH 526/896] Steer touch input flag via bindable rather than config manager --- osu.Game/Input/TouchInputInterceptor.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Input/TouchInputInterceptor.cs b/osu.Game/Input/TouchInputInterceptor.cs index 4e6177c70c..b566113a2a 100644 --- a/osu.Game/Input/TouchInputInterceptor.cs +++ b/osu.Game/Input/TouchInputInterceptor.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; @@ -22,12 +23,17 @@ namespace osu.Game.Input { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - [Resolved] - private SessionStatics statics { get; set; } = null!; + private readonly BindableBool touchInputActive = new BindableBool(); + + [BackgroundDependencyLoader] + private void load(SessionStatics statics) + { + statics.BindWith(Static.TouchInputActive, touchInputActive); + } protected override bool Handle(UIEvent e) { - bool touchInputWasActive = statics.Get(Static.TouchInputActive); + bool touchInputWasActive = touchInputActive.Value; switch (e) { @@ -36,7 +42,7 @@ namespace osu.Game.Input { if (touchInputWasActive) Logger.Log($@"Touch input deactivated due to received {e.GetType().ReadableName()}", LoggingTarget.Input); - statics.SetValue(Static.TouchInputActive, false); + touchInputActive.Value = false; } break; @@ -44,7 +50,7 @@ namespace osu.Game.Input case TouchEvent: if (!touchInputWasActive) Logger.Log($@"Touch input activated due to received {e.GetType().ReadableName()}", LoggingTarget.Input); - statics.SetValue(Static.TouchInputActive, true); + touchInputActive.Value = true; break; case KeyDownEvent keyDown: @@ -59,10 +65,8 @@ namespace osu.Game.Input [Conditional("TOUCH_INPUT_DEBUG")] private void debugToggleTouchInputActive() { - bool oldValue = statics.Get(Static.TouchInputActive); - bool newValue = !oldValue; - Logger.Log($@"Debug-toggling touch input to {(newValue ? @"active" : @"inactive")}", LoggingTarget.Input, LogLevel.Debug); - statics.SetValue(Static.TouchInputActive, newValue); + Logger.Log($@"Debug-toggling touch input to {(touchInputActive.Value ? @"inactive" : @"active")}", LoggingTarget.Input, LogLevel.Debug); + touchInputActive.Toggle(); } } } From adb9ca5a13cb920c6554457392a9f715b85038ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 07:56:09 +0100 Subject: [PATCH 527/896] Add failing test coverage for incorrect treatment of TD in mod presets --- .../UserInterface/TestSceneModPresetColumn.cs | 13 +++++++++++-- .../Visual/UserInterface/TestSceneModPresetPanel.cs | 8 ++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 1779b240cc..b7c1428397 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestAddingFlow() + public void TestAddingFlow([Values] bool withSystemModActive) { ModPresetColumn modPresetColumn = null!; @@ -181,7 +181,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); AddAssert("add preset button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); - AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModDaycore(), new OsuModClassic() }); + AddStep("set mods", () => + { + var newMods = new Mod[] { new OsuModDaycore(), new OsuModClassic() }; + if (withSystemModActive) + newMods = newMods.Append(new OsuModTouchDevice()).ToArray(); + SelectedMods.Value = newMods; + }); AddAssert("add preset button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddStep("click add preset button", () => @@ -209,6 +215,9 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("popover closed", () => !this.ChildrenOfType().Any()); AddUntilStep("preset creation occurred", () => this.ChildrenOfType().Count() == 4); + AddAssert("preset has correct mods", + () => this.ChildrenOfType().Single(panel => panel.Preset.Value.Name == "new preset").Preset.Value.Mods, + () => Has.Count.EqualTo(2)); AddStep("click add preset button", () => { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs index 35e352534b..c79cbd3691 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetPanel.cs @@ -86,6 +86,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set mods to HD+HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() }); AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value); + + // system mods are not included in presets. + AddStep("set mods to HR+DT+TD", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime(), new OsuModTouchDevice() }); + AddAssert("panel is active", () => panel.AsNonNull().Active.Value); } [Test] @@ -113,6 +117,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }); AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); + + AddStep("set system mod", () => SelectedMods.Value = new[] { new OsuModTouchDevice() }); + AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); + assertSelectedModsEquivalentTo(new Mod[] { new OsuModTouchDevice(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); } private void assertSelectedModsEquivalentTo(IEnumerable mods) From 7ba07ab5305dc0be39bddc06967d826e36bfeea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 08:05:42 +0100 Subject: [PATCH 528/896] Add protections against handling system mods in mod presets --- osu.Game/Overlays/Mods/AddPresetPopover.cs | 2 +- osu.Game/Overlays/Mods/EditPresetPopover.cs | 4 ++-- osu.Game/Overlays/Mods/ModPresetPanel.cs | 14 +++++--------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Mods/AddPresetPopover.cs b/osu.Game/Overlays/Mods/AddPresetPopover.cs index 638592a9b5..b782b5d6ba 100644 --- a/osu.Game/Overlays/Mods/AddPresetPopover.cs +++ b/osu.Game/Overlays/Mods/AddPresetPopover.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Mods { Name = nameTextBox.Current.Value, Description = descriptionTextBox.Current.Value, - Mods = selectedMods.Value.ToArray(), + Mods = selectedMods.Value.Where(mod => mod.Type != ModType.System).ToArray(), Ruleset = r.Find(ruleset.Value.ShortName)! })); diff --git a/osu.Game/Overlays/Mods/EditPresetPopover.cs b/osu.Game/Overlays/Mods/EditPresetPopover.cs index 571021b0f8..8bce57c96a 100644 --- a/osu.Game/Overlays/Mods/EditPresetPopover.cs +++ b/osu.Game/Overlays/Mods/EditPresetPopover.cs @@ -153,7 +153,7 @@ namespace osu.Game.Overlays.Mods private void useCurrentMods() { - saveableMods = selectedMods.Value.ToHashSet(); + saveableMods = selectedMods.Value.Where(mod => mod.Type != ModType.System).ToHashSet(); updateState(); } @@ -168,7 +168,7 @@ namespace osu.Game.Overlays.Mods if (!selectedMods.Value.Any()) return false; - return !saveableMods.SetEquals(selectedMods.Value); + return !saveableMods.SetEquals(selectedMods.Value.Where(mod => mod.Type != ModType.System)); } private void save() diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 00f6e36972..3982abeba7 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.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; @@ -56,17 +55,14 @@ namespace osu.Game.Overlays.Mods protected override void Select() { - // if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections, - // which will also have the side effect of activating the preset (see `updateActiveState()`). - selectedMods.Value = Preset.Value.Mods.ToArray(); + var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System); + // will also have the side effect of activating the preset (see `updateActiveState()`). + selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray(); } protected override void Deselect() { - // if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset - // (there are no other active mods than what the preset specifies, and the mod settings match exactly). - // therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off. - selectedMods.Value = Array.Empty(); + selectedMods.Value = selectedMods.Value.Except(Preset.Value.Mods).ToArray(); } private void selectedModsChanged() @@ -79,7 +75,7 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { - Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); + Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value.Where(mod => mod.Type != ModType.System)); } #region Filtering support From 40d081ee2de0940600f94ff126259b019622483b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 16:05:50 +0900 Subject: [PATCH 529/896] Add note about `Width` requirement in `UserGridPanel` --- osu.Game/Users/UserGridPanel.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index f4ec1475b1..aac2315b2f 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -10,6 +10,10 @@ using osuTK; namespace osu.Game.Users { + /// + /// A user "card", commonly used in a grid layout or in popovers. + /// Comes with a preset height, but width must be specified. + /// public partial class UserGridPanel : ExtendedUserPanel { private const int margin = 10; From 1f0b914251bc61d82bf9e3db207e75399a600806 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 16:18:33 +0900 Subject: [PATCH 530/896] Add skin editor dropdown items to reset rotation and scale --- .../Overlays/SkinEditor/SkinSelectionHandler.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index c4e2c4c6bd..df73b15101 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -198,12 +198,26 @@ namespace osu.Game.Overlays.SkinEditor Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray() }; + yield return new EditorMenuItemSpacer(); + yield return new OsuMenuItem("Reset position", MenuItemType.Standard, () => { foreach (var blueprint in SelectedBlueprints) ((Drawable)blueprint.Item).Position = Vector2.Zero; }); + yield return new OsuMenuItem("Reset rotation", MenuItemType.Standard, () => + { + foreach (var blueprint in SelectedBlueprints) + ((Drawable)blueprint.Item).Rotation = 0; + }); + + yield return new OsuMenuItem("Reset scale", MenuItemType.Standard, () => + { + foreach (var blueprint in SelectedBlueprints) + ((Drawable)blueprint.Item).Scale = Vector2.One; + }); + yield return new EditorMenuItemSpacer(); yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront()); From 0915ac8891f1a5036a325d95b870d29c0daeb733 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 16:32:12 +0900 Subject: [PATCH 531/896] Use left aligned text for non-rotate key counter --- osu.Game/Screens/Play/ArgonKeyCounter.cs | 28 +++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/ArgonKeyCounter.cs b/osu.Game/Screens/Play/ArgonKeyCounter.cs index bb5fe0daf2..874fcde329 100644 --- a/osu.Game/Screens/Play/ArgonKeyCounter.cs +++ b/osu.Game/Screens/Play/ArgonKeyCounter.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.Graphics; using osu.Framework.Graphics.Containers; @@ -18,6 +19,8 @@ namespace osu.Game.Screens.Play private OsuSpriteText keyNameText = null!; private OsuSpriteText countText = null!; + private UprightAspectMaintainingContainer uprightContainer = null!; + // These values were taken from Figma private const float line_height = 3; private const float name_font_size = 10; @@ -53,7 +56,7 @@ namespace osu.Game.Screens.Play Padding = new MarginPadding { Top = line_height * scale_factor + indicator_press_offset }, Children = new Drawable[] { - new UprightAspectMaintainingContainer + uprightContainer = new UprightAspectMaintainingContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -62,16 +65,16 @@ namespace osu.Game.Screens.Play { keyNameText = new OsuSpriteText { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold), Colour = colours.Blue0, Text = Trigger.Name }, countText = new OsuSpriteText { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold), }, } @@ -93,6 +96,21 @@ namespace osu.Game.Screens.Play CountPresses.BindValueChanged(e => countText.Text = e.NewValue.ToString(@"#,0"), true); } + protected override void Update() + { + base.Update(); + + const float allowance = 6; + float absRotation = Math.Abs(uprightContainer.Rotation) % 180; + bool isRotated = absRotation > allowance && absRotation < (180 - allowance); + + keyNameText.Anchor = + keyNameText.Origin = isRotated ? Anchor.TopCentre : Anchor.TopLeft; + + countText.Anchor = + countText.Origin = isRotated ? Anchor.BottomCentre : Anchor.BottomLeft; + } + protected override void Activate(bool forwardPlayback = true) { base.Activate(forwardPlayback); From b45d8c785cd662796c6098f72aadf589b99161e4 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Mon, 6 Nov 2023 08:38:34 +0100 Subject: [PATCH 532/896] fixed review findings --- .../Online/TestSceneUserClickableAvatar.cs | 26 +++++++++++++++++++ osu.Game/Users/Drawables/ClickableAvatar.cs | 21 +++++++++++++++ osu.Game/Users/Drawables/UpdateableAvatar.cs | 1 + 3 files changed, 48 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 13f559ac09..a24581f7ed 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -78,6 +78,8 @@ namespace osu.Game.Tests.Visual.Online Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), }, }, + new ClickableAvatar(), + new ClickableAvatar(), }, }; }); @@ -120,6 +122,30 @@ namespace osu.Game.Tests.Visual.Online AddWaitStep("wait for tooltip to show", 5); AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); AddWaitStep("wait for tooltip to hide", 3); + + AddStep($"click null user {4}. {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 4) + return; + + InputManager.MoveMouseTo(targets[3]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); + + AddStep($"click null user {5}. {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 5) + return; + + InputManager.MoveMouseTo(targets[4]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); } } } diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index d6c6afba0b..0ed9f56cc7 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -1,12 +1,15 @@ // 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.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -21,6 +24,24 @@ namespace osu.Game.Users.Drawables Width = 300 }; + public override LocalisableString TooltipText + { + get + { + if (!Enabled.Value) + return string.Empty; + + return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile; + } + set => throw new NotSupportedException(); + } + + /// + /// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username. + /// Setting this to true exposes the username via tooltip for special cases where this is not true. + /// + public bool ShowUsernameTooltip { get; set; } + private readonly APIUser? user; [Resolved] diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 58b3646995..711e7ab799 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -75,6 +75,7 @@ namespace osu.Game.Users.Drawables return new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, + ShowUsernameTooltip = showUsernameTooltip, }; } else From 69d6feb5a8b9137aaf74956945b897d7741fe3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 08:51:31 +0100 Subject: [PATCH 533/896] Add test coverage for player name skin component --- .../Archives/modified-argon-20231106.osk | Bin 0 -> 1397 bytes osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20231106.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20231106.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20231106.osk new file mode 100644 index 0000000000000000000000000000000000000000..70c4ff64d74b2efe0cd630812a41186f11f0467d GIT binary patch literal 1397 zcmWIWW@Zs#U|`^2`0JV$epO>m++rZ_AXtQfp*TA;PcJhsQ?+L!*C7W1*5ALomQQ8$ zRN?Tl(ze##D%fqh-IMQq+0!D$AGu<6llCVZ$xCSax#ym1WAgEgE7h`dx2y;i@;>BY zo8cj)v2)g*`ldCh-M58r@f+Vz{8uC{Cv{oK{FVC3rel?2Ts@D>x5@}wOkC=+bLX2# zi%I)u9S#Zke{oLIYNO+eVy33HxdyzJI$n0@t(Ea^#ZoElIZyPgx-5&|%=KzDx<5ld z0L3F)-r3fd0zJ_S#JoTZ@z} ze0N~b1p^}kqe~$dPMq~W>2uo0m(fVq$7E`WL)4t^AYt!M-445)K5zQ`>GWrXNAoI= zs&-bY&g@j3`Ll9fW#-R-;;(la%U-air$2j|`fTZI%ceC9A4^0MCV9ndWk7YG!T++N zdL{;jQ^E`kVnFx#CT8Y>|EA=N9DWrRJ3sgB|*Ix^Mn12Z6Ttf4I7hBN|o;ho4ex z-L`VowiPOST_$cdYu=l>w>j;)O4yP5_qQxMJ?}baG`(!MR5;IL{J8d;?B?ydr?dJy zV>de-V7EN}E&KJg8$G5%5(bIy^t-E9ZxP@3IfwJuWw)uf-bXThGf`n!`KB{rW^I|_ zw&MorD=#SfFv%opnFbu+#r;0weuId6<)zKt{>PDj^TEUY6Xrf< zdeZ6Bno%xiHDke}TPZuh)p_#HM-6-)cAcKz?=l;;-K z%xlUHocj7LJw}(e`%h%n?Mk~#%S9I2Fsqf=+!M(Sna}R|-TX=9TZM!t#}6!ykUsz5 z4MKBujhA8uxmWH z{Ed0fHl5y6TVDPT*sAo_zTad2^35DV6K;veG_6mT(_1pLTP^R$ikkwg%bq-Mu0ATt zzvF?G_R_tg5ghghr>=aySyZ5Vfu4>@DCeJ+i%ciNt+cPL*)OUb!SLwd_e1&Z^~u6z z<+GI!|KD9;zq^3R#NBHKPhwL{Rw)Pj$z}1$>{SzD3N(G)s#{J9ExqX_-RCf$+sAj_ zpZ@lG$&Fd;#Z!J|2I=fuZx(2I!20>Ft^XKNldR0tq$)LFQq=@vJ|GUrNvurEOwCC_ z57ZD2Ur>zzPW&%bg5eD4l4bWH!XarHP z(g$4^dgg}eVPI%H3DpJ9>gZb0^BqEKCL^v~h;9aYKp@PJVL=a;0B=?{kUR?zegx8a IKotxO0R77}LjV8( literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index 98008a003d..67f9d20fce 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -54,7 +54,9 @@ namespace osu.Game.Tests.Skins // Covers key counters "Archives/modified-argon-pro-20230618.osk", // Covers "Argon" health display - "Archives/modified-argon-pro-20231001.osk" + "Archives/modified-argon-pro-20231001.osk", + // Covers player name text component. + "Archives/modified-argon-20231106.osk", }; /// From b62811633f07820848ec960e228989f02e30f68c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 17:17:19 +0900 Subject: [PATCH 534/896] Add test coverage of touching and missing not enabled touch mod --- .../Mods/TestSceneOsuModTouchDevice.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index 5134265741..bcfa407684 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -104,6 +104,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods AddAssert("no toasts displayed", () => testOnScreenDisplay.ToastCount, () => Is.Zero); } + [Test] + public void TestTouchMiss() + { + // ensure mouse is active (and that it's not suppressed due to touches in previous tests) + AddStep("click mouse", () => InputManager.Click(MouseButton.Left)); + + AddUntilStep("wait until 200 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200).Within(500)); + AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); + AddUntilStep("wait until 200", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200)); + AddStep("touch playfield", () => + { + var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod not activated", () => Player.Mods.Value, () => Has.No.InstanceOf()); + } + [Test] public void TestSecondObjectTouched() { From 4a70f2435c574b82829a5d2508ae29f7d2e3a2fb Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Mon, 6 Nov 2023 09:22:50 +0100 Subject: [PATCH 535/896] fixed showUsernameTooltip --- .../OnlinePlay/Components/ParticipantsList.cs | 2 +- .../DrawableRoomParticipantsList.cs | 2 +- osu.Game/Users/Drawables/ClickableAvatar.cs | 21 ------------------- osu.Game/Users/Drawables/UpdateableAvatar.cs | 6 +----- 4 files changed, 3 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index 00f0889cc8..cb1a846d6c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Components RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex(@"27252d"), }, - avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both }, + avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }, }; } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 06f9f35479..1814f5359f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -289,7 +289,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components set => avatar.User = value; } - private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both }; + private readonly UpdateableAvatar avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 0ed9f56cc7..d6c6afba0b 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -1,15 +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.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; -using osu.Framework.Localisation; using osu.Game.Graphics.Containers; -using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -24,24 +21,6 @@ namespace osu.Game.Users.Drawables Width = 300 }; - public override LocalisableString TooltipText - { - get - { - if (!Enabled.Value) - return string.Empty; - - return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile; - } - set => throw new NotSupportedException(); - } - - /// - /// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username. - /// Setting this to true exposes the username via tooltip for special cases where this is not true. - /// - public bool ShowUsernameTooltip { get; set; } - private readonly APIUser? user; [Resolved] diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 711e7ab799..3c72d7f7e0 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -46,7 +46,6 @@ namespace osu.Game.Users.Drawables protected override double LoadDelay => 200; private readonly bool isInteractive; - private readonly bool showUsernameTooltip; private readonly bool showGuestOnNull; /// @@ -54,12 +53,10 @@ namespace osu.Game.Users.Drawables /// /// The initial user to display. /// If set to true, hover/click sounds will play and clicking the avatar will open the user's profile. - /// Whether to show the username rather than "view profile" on the tooltip. (note: this only applies if is also true) /// Whether to show a default guest representation on null user (as opposed to nothing). - public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true) + public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showGuestOnNull = true) { this.isInteractive = isInteractive; - this.showUsernameTooltip = showUsernameTooltip; this.showGuestOnNull = showGuestOnNull; User = user; @@ -75,7 +72,6 @@ namespace osu.Game.Users.Drawables return new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, - ShowUsernameTooltip = showUsernameTooltip, }; } else From e2928cc6b96f816320f238e5f653bd75c5ed9ca3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 17:32:39 +0900 Subject: [PATCH 536/896] Fix incorrect assertion check targets (and flip assertion for miss case) --- .../Mods/TestSceneOsuModTouchDevice.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index bcfa407684..b77cc038c9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); - AddAssert("touch device mod not activated", () => Player.Mods.Value, () => Has.No.InstanceOf()); + AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); } [Test] @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); InputManager.Click(MouseButton.Left); }); - AddAssert("touch device mod not activated", () => Player.Mods.Value, () => Has.No.InstanceOf()); + AddAssert("touch device mod not activated", () => Player.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); AddStep("speed back up", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 1); AddUntilStep("wait until 5000 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000).Within(500)); From 97fee6143c7b81d169452396a490b8293b521527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 10:08:19 +0100 Subject: [PATCH 537/896] Rename touch "input handlers" to detectors --- .../{PlayerTouchInputHandler.cs => PlayerTouchInputDetector.cs} | 2 +- osu.Game/Screens/Play/SubmittingPlayer.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- ...lectTouchInputHandler.cs => SongSelectTouchInputDetector.cs} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Screens/Play/{PlayerTouchInputHandler.cs => PlayerTouchInputDetector.cs} (97%) rename osu.Game/Screens/Select/{SongSelectTouchInputHandler.cs => SongSelectTouchInputDetector.cs} (96%) diff --git a/osu.Game/Screens/Play/PlayerTouchInputHandler.cs b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs similarity index 97% rename from osu.Game/Screens/Play/PlayerTouchInputHandler.cs rename to osu.Game/Screens/Play/PlayerTouchInputDetector.cs index a7d41de105..8bef24b66c 100644 --- a/osu.Game/Screens/Play/PlayerTouchInputHandler.cs +++ b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play { - public partial class PlayerTouchInputHandler : Component + public partial class PlayerTouchInputDetector : Component { [Resolved] private Player player { get; set; } = null!; diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index e018b8dab3..30fecbe149 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play return; } - AddInternal(new PlayerTouchInputHandler()); + AddInternal(new PlayerTouchInputDetector()); } protected override void LoadAsyncComplete() diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 74454713d1..03083672d5 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -279,7 +279,7 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.Both, }, - new SongSelectTouchInputHandler() + new SongSelectTouchInputDetector() }); if (ShowFooter) diff --git a/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs b/osu.Game/Screens/Select/SongSelectTouchInputDetector.cs similarity index 96% rename from osu.Game/Screens/Select/SongSelectTouchInputHandler.cs rename to osu.Game/Screens/Select/SongSelectTouchInputDetector.cs index 973dc12e12..b726acb45f 100644 --- a/osu.Game/Screens/Select/SongSelectTouchInputHandler.cs +++ b/osu.Game/Screens/Select/SongSelectTouchInputDetector.cs @@ -13,7 +13,7 @@ using osu.Game.Utils; namespace osu.Game.Screens.Select { - public partial class SongSelectTouchInputHandler : Component + public partial class SongSelectTouchInputDetector : Component { [Resolved] private Bindable ruleset { get; set; } = null!; From 204cd541e2b8876089c619abd94e97baafb97b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 10:14:56 +0100 Subject: [PATCH 538/896] Use placeholder mod icon for touch device --- osu.Game/Rulesets/Mods/ModTouchDevice.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs index a5dfe5448c..b80b042f11 100644 --- a/osu.Game/Rulesets/Mods/ModTouchDevice.cs +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { @@ -10,6 +12,7 @@ namespace osu.Game.Rulesets.Mods { public sealed override string Name => "Touch Device"; public sealed override string Acronym => "TD"; + public sealed override IconUsage? Icon => OsuIcon.PlayStyleTouch; public sealed override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; public sealed override double ScoreMultiplier => 1; public sealed override ModType Type => ModType.System; From aa6f14b0247f2fe0768379b9a1ae03f75827ba86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 18:16:04 +0900 Subject: [PATCH 539/896] Fix spinner test hitting assertion when spinning too fast --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index ea57a6a1b5..9980e1a55f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -153,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Tests { base.Update(); if (auto) - RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * spinRate.Value)); + RotationTracker.AddRotation((float)Math.Min(180, Clock.ElapsedFrameTime * spinRate.Value)); } } } From 86cf0a36cfb842cf97f074988a28c9958e9b0b81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 18:16:31 +0900 Subject: [PATCH 540/896] Add test coverage of spinner with no bonus ticks --- .../TestSceneSpinner.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 9980e1a55f..77b16dd0c5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -37,6 +37,12 @@ namespace osu.Game.Rulesets.Osu.Tests AddSliderStep("Spin rate", 0.5, 5, 1, val => spinRate.Value = val); } + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Reset rate", () => spinRate.Value = 1); + } + [TestCase(true)] [TestCase(false)] public void TestVariousSpinners(bool autoplay) @@ -47,6 +53,36 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep($"{term} Small", () => SetContents(_ => testSingle(7, autoplay))); } + [Test] + public void TestSpinnerNoBonus() + { + AddStep("Set high spin rate", () => spinRate.Value = 5); + + Spinner spinner; + + AddStep("add spinner", () => SetContents(_ => + { + spinner = new Spinner + { + StartTime = Time.Current, + EndTime = Time.Current + 750, + Samples = new List + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + } + }; + + spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = 0 }); + + return drawableSpinner = new TestDrawableSpinner(spinner, true, spinRate) + { + Anchor = Anchor.Centre, + Depth = depthIndex++, + Scale = new Vector2(0.75f) + }; + })); + } + [Test] public void TestSpinningSamplePitchShift() { From b219a371a93d82c3638df3b47f51e85d20f1fb94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 18:29:51 +0900 Subject: [PATCH 541/896] Move sample playback logic local to avoid edge case with no bonus ticks Can't see a better way of doing this. --- .../Objects/Drawables/DrawableSpinner.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index aa43532f65..e159d06a02 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -45,6 +45,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; + private SkinnableSound maxBonusSample; + /// /// The amount of bonus score gained from spinning after the required number of spins, for display purposes. /// @@ -109,6 +111,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, Looping = true, Frequency = { Value = spinning_sample_initial_frequency } + }, + maxBonusSample = new SkinnableSound + { + MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME, } }); @@ -128,6 +134,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnFree(); spinningSample.ClearSamples(); + maxBonusSample.ClearSamples(); } protected override void LoadSamples() @@ -136,6 +143,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables spinningSample.Samples = HitObject.CreateSpinningSamples().Cast().ToArray(); spinningSample.Frequency.Value = spinning_sample_initial_frequency; + + maxBonusSample.Samples = new ISampleInfo[] { HitObject.CreateHitSampleInfo("spinnerbonus") }; } private void updateSpinningSample(ValueChangedEvent tracking) @@ -157,6 +166,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.StopAllSamples(); spinningSample?.Stop(); + maxBonusSample?.Stop(); } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -303,8 +313,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; - private int lastMaxSamplePlayback; - private void updateBonusScore() { if (ticks.Count == 0) @@ -327,9 +335,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (tick == null) { // we still want to play a sound. this will probably be a new sound in the future, but for now let's continue playing the bonus sound. - // round robin to avoid hitting playback concurrency. - tick = ticks.OfType().Skip(lastMaxSamplePlayback++ % HitObject.MaximumBonusSpins).First(); - tick.PlaySamples(); + // TODO: this doesn't concurrency. i can't figure out how to make it concurrency. samples are bad and need a refactor. + maxBonusSample.Play(); } else tick.TriggerResult(true); From 92e4a8666def6e4bcc66d008ca0d206ca6c6cbd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 18:43:47 +0900 Subject: [PATCH 542/896] Add `spinnerbonus-max` support and fallback to `spinnerbonus` --- .../Objects/Drawables/DrawableSpinner.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index e159d06a02..c0c135d145 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -144,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables spinningSample.Samples = HitObject.CreateSpinningSamples().Cast().ToArray(); spinningSample.Frequency.Value = spinning_sample_initial_frequency; - maxBonusSample.Samples = new ISampleInfo[] { HitObject.CreateHitSampleInfo("spinnerbonus") }; + maxBonusSample.Samples = new ISampleInfo[] { new SpinnerBonusMaxSampleInfo(HitObject.CreateHitSampleInfo()) }; } private void updateSpinningSample(ValueChangedEvent tracking) @@ -344,5 +345,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables completedFullSpins.Value++; } } + + public class SpinnerBonusMaxSampleInfo : HitSampleInfo + { + public override IEnumerable LookupNames + { + get + { + foreach (string name in base.LookupNames) + yield return name; + + foreach (string name in base.LookupNames) + yield return name.Replace("-max", string.Empty); + } + } + + public SpinnerBonusMaxSampleInfo(HitSampleInfo sampleInfo) + : base("spinnerbonus-max", sampleInfo.Bank, sampleInfo.Suffix, sampleInfo.Volume) + + { + } + } } } From 682668ccf0f73bdbb98b800afcaff983399d391d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 10:54:32 +0100 Subject: [PATCH 543/896] Remove touch device toasts entirely --- .../Mods/TestSceneOsuModTouchDevice.cs | 21 ------------------- .../Overlays/OSD/TouchDeviceDetectedToast.cs | 15 ------------- .../Screens/Play/PlayerTouchInputDetector.cs | 12 ----------- 3 files changed, 48 deletions(-) delete mode 100644 osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index b77cc038c9..e41cc8cfbc 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -4,13 +4,11 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; using osu.Game.Configuration; using osu.Game.Input; -using osu.Game.Overlays; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -25,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [Resolved] private SessionStatics statics { get; set; } = null!; - private TestOnScreenDisplay testOnScreenDisplay = null!; - protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => @@ -54,14 +50,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [BackgroundDependencyLoader] private void load() { - Add(testOnScreenDisplay = new TestOnScreenDisplay()); Add(new TouchInputInterceptor()); - Dependencies.CacheAs(testOnScreenDisplay); } public override void SetUpSteps() { - AddStep("reset OSD toast count", () => testOnScreenDisplay.ToastCount = 0); AddStep("reset static", () => statics.SetValue(Static.TouchInputActive, false)); base.SetUpSteps(); } @@ -85,7 +78,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods InputManager.EndTouch(touch); }); AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); - AddAssert("no toasts displayed", () => testOnScreenDisplay.ToastCount, () => Is.Zero); } [Test] @@ -101,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods InputManager.EndTouch(touch); }); AddAssert("touch device mod not activated", () => Player.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); - AddAssert("no toasts displayed", () => testOnScreenDisplay.ToastCount, () => Is.Zero); } [Test] @@ -149,18 +140,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods InputManager.EndTouch(touch); }); AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); - AddAssert("toast displayed", () => testOnScreenDisplay.ToastCount, () => Is.EqualTo(1)); - } - - private partial class TestOnScreenDisplay : OnScreenDisplay - { - public int ToastCount { get; set; } - - protected override void DisplayTemporarily(Drawable toDisplay) - { - base.DisplayTemporarily(toDisplay); - ToastCount++; - } } } } diff --git a/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs b/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs deleted file mode 100644 index 266e10ab1f..0000000000 --- a/osu.Game/Overlays/OSD/TouchDeviceDetectedToast.cs +++ /dev/null @@ -1,15 +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.Rulesets; - -namespace osu.Game.Overlays.OSD -{ - public partial class TouchDeviceDetectedToast : Toast - { - public TouchDeviceDetectedToast(RulesetInfo ruleset) - : base(ruleset.Name, "Touch device detected", "Touch Device mod applied to score") - { - } - } -} diff --git a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs index 8bef24b66c..a5055dfb00 100644 --- a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs +++ b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs @@ -6,10 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Configuration; -using osu.Game.Overlays; -using osu.Game.Overlays.OSD; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play { @@ -21,9 +18,6 @@ namespace osu.Game.Screens.Play [Resolved] private GameplayState gameplayState { get; set; } = null!; - [Resolved] - private OnScreenDisplay? onScreenDisplay { get; set; } - private IBindable touchActive = new BindableBool(); [BackgroundDependencyLoader] @@ -51,12 +45,6 @@ namespace osu.Game.Screens.Play if (touchDeviceMod == null) return; - // do not show the toast if the user hasn't hit anything yet. - // we're kind of assuming that the user just switches to touch for gameplay - // and we don't want to spam them with obvious toasts. - if (gameplayState.ScoreProcessor.HitEvents.Any(ev => ev.Result.IsHit())) - onScreenDisplay?.Display(new TouchDeviceDetectedToast(gameplayState.Ruleset.RulesetInfo)); - // `Player` (probably rightly so) assumes immutability of mods, // so this will not be shown immediately on the mod display in the top right. // if this is to change, the mod immutability should be revisited. From 718492a0b7d9c38b32102095c84393fbe5a792d9 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Mon, 6 Nov 2023 11:29:15 +0100 Subject: [PATCH 544/896] fixed DRY --- .../Online/TestSceneUserClickableAvatar.cs | 83 +++++++------------ 1 file changed, 32 insertions(+), 51 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index a24581f7ed..72870a5647 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -27,59 +27,14 @@ namespace osu.Game.Tests.Visual.Online Anchor = Anchor.Centre, Origin = Anchor.Centre, Spacing = new Vector2(10f), - Children = new Drawable[] + Children = new[] { - new ClickableAvatar(new APIUser - { - Username = @"flyte", Id = 3103765, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" - }) - { - Width = 50, - Height = 50, - CornerRadius = 10, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), - }, - }, - new ClickableAvatar(new APIUser - { - Username = @"peppy", Id = 2, Colour = "99EB47", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - }) - { - Width = 50, - Height = 50, - CornerRadius = 10, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), - }, - }, - new ClickableAvatar(new APIUser - { - Username = @"flyte", - Id = 3103765, - CountryCode = CountryCode.JP, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", - Status = - { - Value = new UserStatusOnline() - } - }) - { - Width = 50, - Height = 50, - CornerRadius = 10, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), - }, - }, - new ClickableAvatar(), + generateUser(@"peppy", 2, CountryCode.AU, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", "99EB47"), + generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"), + generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"), new ClickableAvatar(), + new UpdateableAvatar(), + new UpdateableAvatar(), }, }; }); @@ -147,5 +102,31 @@ namespace osu.Game.Tests.Visual.Online AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); AddWaitStep("wait for tooltip to hide", 3); } + + private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, string? color = null) + { + return new ClickableAvatar(new APIUser + { + Username = username, + Id = id, + CountryCode = countryCode, + CoverUrl = cover, + Colour = color ?? "000000", + Status = + { + Value = new UserStatusOnline() + } + }) + { + Width = 50, + Height = 50, + CornerRadius = 10, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), + }, + }; + } } } From 51c891e2e4ff17432a01bf4f9ee75ad8c1498883 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Nov 2023 19:34:34 +0900 Subject: [PATCH 545/896] Automatically refresh the verify screen's issue list on re-entering it Addresses https://github.com/ppy/osu/discussions/25365. --- osu.Game/Screens/Edit/Verify/IssueList.cs | 10 +++++----- osu.Game/Screens/Edit/Verify/VerifyScreen.cs | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 907949aee8..d07190fca0 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Edit.Verify new RoundedButton { Text = "Refresh", - Action = refresh, + Action = Refresh, Size = new Vector2(120, 40), Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -86,13 +86,13 @@ namespace osu.Game.Screens.Edit.Verify { base.LoadComplete(); - verify.InterpretedDifficulty.BindValueChanged(_ => refresh()); - verify.HiddenIssueTypes.BindCollectionChanged((_, _) => refresh()); + verify.InterpretedDifficulty.BindValueChanged(_ => Refresh()); + verify.HiddenIssueTypes.BindCollectionChanged((_, _) => Refresh()); - refresh(); + Refresh(); } - private void refresh() + public void Refresh() { var issues = generalVerifier.Run(context); diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index b17cf3379e..b6e0450e23 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -56,5 +56,11 @@ namespace osu.Game.Screens.Edit.Verify } }; } + + protected override void PopIn() + { + base.PopIn(); + IssueList.Refresh(); + } } } From 6deac9a5a45319d1536a9dc223a4efb07324a63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 11:24:56 +0100 Subject: [PATCH 546/896] Use better colours for system mods --- osu.Game/Graphics/OsuColour.cs | 2 +- osu.Game/Rulesets/UI/ModIcon.cs | 6 ++++-- osu.Game/Rulesets/UI/ModSwitchSmall.cs | 8 ++++++-- osu.Game/Rulesets/UI/ModSwitchTiny.cs | 6 +++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 75d313d98c..2e19eac572 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -162,7 +162,7 @@ namespace osu.Game.Graphics return Pink1; case ModType.System: - return Gray7; + return Gray5; default: throw new ArgumentOutOfRangeException(nameof(modType), modType, "Unknown mod type"); diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 5fd1507039..d09db37f2a 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -138,7 +138,6 @@ namespace osu.Game.Rulesets.UI { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Colour = OsuColour.Gray(84), Alpha = 0, Font = OsuFont.Numeric.With(null, 22f), UseFullGlyphHeight = false, @@ -148,7 +147,6 @@ namespace osu.Game.Rulesets.UI { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Colour = OsuColour.Gray(84), Size = new Vector2(45), Icon = FontAwesome.Solid.Question }, @@ -206,6 +204,10 @@ namespace osu.Game.Rulesets.UI private void updateColour() { + modAcronym.Colour = modIcon.Colour = mod.Type != ModType.System + ? OsuColour.Gray(84) + : colours.Yellow; + extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour; extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f); } diff --git a/osu.Game/Rulesets/UI/ModSwitchSmall.cs b/osu.Game/Rulesets/UI/ModSwitchSmall.cs index 927379c684..452a5599ba 100644 --- a/osu.Game/Rulesets/UI/ModSwitchSmall.cs +++ b/osu.Game/Rulesets/UI/ModSwitchSmall.cs @@ -85,11 +85,15 @@ namespace osu.Game.Rulesets.UI tinySwitch.Scale = new Vector2(0.3f); } + var modTypeColour = colours.ForModType(mod.Type); + inactiveForegroundColour = colourProvider?.Background5 ?? colours.Gray3; - activeForegroundColour = colours.ForModType(mod.Type); + activeForegroundColour = mod.Type != ModType.System ? modTypeColour : colours.Yellow; inactiveBackgroundColour = colourProvider?.Background2 ?? colours.Gray5; - activeBackgroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1); + activeBackgroundColour = mod.Type != ModType.System + ? Interpolation.ValueAt(0.1f, Colour4.Black, modTypeColour, 0, 1) + : modTypeColour; } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index a3e325ace8..5bf501ead3 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -106,11 +106,15 @@ namespace osu.Game.Rulesets.UI [BackgroundDependencyLoader(true)] private void load(OsuColour colours, OverlayColourProvider? colourProvider) { + var modTypeColour = colours.ForModType(Mod.Type); + inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3; activeBackgroundColour = colours.ForModType(Mod.Type); inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5; - activeForegroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1); + activeForegroundColour = Mod.Type != ModType.System + ? Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1) + : colours.Yellow; } protected override void LoadComplete() From 39ad91feea58e0a91e95b3a2be0889ff98be4ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 11:50:04 +0100 Subject: [PATCH 547/896] Make debug input toggle post notifications --- osu.Game/Input/TouchInputInterceptor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/TouchInputInterceptor.cs b/osu.Game/Input/TouchInputInterceptor.cs index b566113a2a..368d8469ae 100644 --- a/osu.Game/Input/TouchInputInterceptor.cs +++ b/osu.Game/Input/TouchInputInterceptor.cs @@ -65,7 +65,7 @@ namespace osu.Game.Input [Conditional("TOUCH_INPUT_DEBUG")] private void debugToggleTouchInputActive() { - Logger.Log($@"Debug-toggling touch input to {(touchInputActive.Value ? @"inactive" : @"active")}", LoggingTarget.Input, LogLevel.Debug); + Logger.Log($@"Debug-toggling touch input to {(touchInputActive.Value ? @"inactive" : @"active")}", LoggingTarget.Information, LogLevel.Important); touchInputActive.Toggle(); } } From 034f53da4b1d79220d8af95055a203076ab88c87 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Mon, 6 Nov 2023 11:54:57 +0100 Subject: [PATCH 548/896] added isEnabled to tooltip --- osu.Game/Users/Drawables/ClickableAvatar.cs | 16 +++++++++++++++- osu.Game/Users/Drawables/UpdateableAvatar.cs | 5 ++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index d6c6afba0b..0520f62665 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -14,13 +14,15 @@ namespace osu.Game.Users.Drawables { public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip { - public ITooltip GetCustomTooltip() => new UserGridPanelTooltip(); + public ITooltip GetCustomTooltip() => new UserGridPanelTooltip(IsTooltipEnabled); public UserGridPanel TooltipContent => new UserGridPanel(user!) { Width = 300 }; + public bool IsTooltipEnabled; + private readonly APIUser? user; [Resolved] @@ -33,6 +35,7 @@ namespace osu.Game.Users.Drawables public ClickableAvatar(APIUser? user = null) { this.user = user; + IsTooltipEnabled = true; if (user?.Id != APIUser.SYSTEM_USER_ID) Action = openProfile; @@ -60,10 +63,21 @@ namespace osu.Game.Users.Drawables private partial class UserGridPanelTooltip : VisibilityContainer, ITooltip { + private readonly bool isEnabled; private UserGridPanel? displayedUser; + public UserGridPanelTooltip(bool isEnabled = true) + { + this.isEnabled = isEnabled; + } + protected override void PopIn() { + if (displayedUser is null || !isEnabled) + { + return; + } + Child = displayedUser; this.FadeIn(20, Easing.OutQuint); } diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 3c72d7f7e0..a970997056 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -47,17 +47,20 @@ namespace osu.Game.Users.Drawables private readonly bool isInteractive; private readonly bool showGuestOnNull; + private readonly bool showUserPanel; /// /// Construct a new UpdateableAvatar. /// /// The initial user to display. /// If set to true, hover/click sounds will play and clicking the avatar will open the user's profile. + /// If set to true, the user status panel will be displayed in the tooltip. /// Whether to show a default guest representation on null user (as opposed to nothing). - public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showGuestOnNull = true) + public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUserPanel = true, bool showGuestOnNull = true) { this.isInteractive = isInteractive; this.showGuestOnNull = showGuestOnNull; + this.showUserPanel = showUserPanel; User = user; } From 4bc36a6c90dfd051cd6202be7e4fec48c1b05080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 12:18:02 +0100 Subject: [PATCH 549/896] Fix unused variable --- osu.Game/Rulesets/UI/ModSwitchTiny.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index 5bf501ead3..bb121c085c 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -109,11 +109,11 @@ namespace osu.Game.Rulesets.UI var modTypeColour = colours.ForModType(Mod.Type); inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3; - activeBackgroundColour = colours.ForModType(Mod.Type); + activeBackgroundColour = modTypeColour; inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5; activeForegroundColour = Mod.Type != ModType.System - ? Interpolation.ValueAt(0.1f, Colour4.Black, activeForegroundColour, 0, 1) + ? Interpolation.ValueAt(0.1f, Colour4.Black, modTypeColour, 0, 1) : colours.Yellow; } From a01f6187f4738f85376cb820315d95d8c4545e4c Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Mon, 6 Nov 2023 14:52:06 +0100 Subject: [PATCH 550/896] testing the tooltip --- .../Online/TestSceneUserClickableAvatar.cs | 134 +++++++++--------- osu.Game/Users/Drawables/ClickableAvatar.cs | 43 +++++- osu.Game/Users/Drawables/UpdateableAvatar.cs | 1 + 3 files changed, 102 insertions(+), 76 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 72870a5647..678767f15e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -1,13 +1,11 @@ // 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.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; -using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -29,12 +27,9 @@ namespace osu.Game.Tests.Visual.Online Spacing = new Vector2(10f), Children = new[] { - generateUser(@"peppy", 2, CountryCode.AU, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", "99EB47"), - generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"), - generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"), - new ClickableAvatar(), - new UpdateableAvatar(), - new UpdateableAvatar(), + generateUser(@"peppy", 2, CountryCode.AU, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false, "99EB47"), + generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", false), + generateUser(@"joshika39", 17032217, CountryCode.RS, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", true), }, }; }); @@ -42,68 +37,68 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestClickableAvatarHover() { - AddStep($"click {1}. {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 1) - return; - - InputManager.MoveMouseTo(targets[0]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); - - AddStep($"click {2}. {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 2) - return; - - InputManager.MoveMouseTo(targets[1]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); - - AddStep($"click {3}. {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 3) - return; - - InputManager.MoveMouseTo(targets[2]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); - - AddStep($"click null user {4}. {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 4) - return; - - InputManager.MoveMouseTo(targets[3]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); - - AddStep($"click null user {5}. {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 5) - return; - - InputManager.MoveMouseTo(targets[4]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); + // AddStep($"click {1}. {nameof(ClickableAvatar)}", () => + // { + // var targets = this.ChildrenOfType().ToList(); + // if (targets.Count < 1) + // return; + // + // InputManager.MoveMouseTo(targets[0]); + // }); + // AddWaitStep("wait for tooltip to show", 5); + // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + // AddWaitStep("wait for tooltip to hide", 3); + // + // AddStep($"click {2}. {nameof(ClickableAvatar)}", () => + // { + // var targets = this.ChildrenOfType().ToList(); + // if (targets.Count < 2) + // return; + // + // InputManager.MoveMouseTo(targets[1]); + // }); + // AddWaitStep("wait for tooltip to show", 5); + // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + // AddWaitStep("wait for tooltip to hide", 3); + // + // AddStep($"click {3}. {nameof(ClickableAvatar)}", () => + // { + // var targets = this.ChildrenOfType().ToList(); + // if (targets.Count < 3) + // return; + // + // InputManager.MoveMouseTo(targets[2]); + // }); + // AddWaitStep("wait for tooltip to show", 5); + // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + // AddWaitStep("wait for tooltip to hide", 3); + // + // AddStep($"click null user {4}. {nameof(ClickableAvatar)}", () => + // { + // var targets = this.ChildrenOfType().ToList(); + // if (targets.Count < 4) + // return; + // + // InputManager.MoveMouseTo(targets[3]); + // }); + // AddWaitStep("wait for tooltip to show", 5); + // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + // AddWaitStep("wait for tooltip to hide", 3); + // + // AddStep($"click null user {5}. {nameof(ClickableAvatar)}", () => + // { + // var targets = this.ChildrenOfType().ToList(); + // if (targets.Count < 5) + // return; + // + // InputManager.MoveMouseTo(targets[4]); + // }); + // AddWaitStep("wait for tooltip to show", 5); + // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + // AddWaitStep("wait for tooltip to hide", 3); } - private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, string? color = null) + private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool isTooltipEnabled, string? color = null) { return new ClickableAvatar(new APIUser { @@ -115,7 +110,7 @@ namespace osu.Game.Tests.Visual.Online Status = { Value = new UserStatusOnline() - } + }, }) { Width = 50, @@ -126,6 +121,7 @@ namespace osu.Game.Tests.Visual.Online { Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), }, + IsTooltipEnabled = isTooltipEnabled, }; } } diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 0520f62665..c11ad7f720 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -1,12 +1,15 @@ // 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.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; +using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -14,14 +17,32 @@ namespace osu.Game.Users.Drawables { public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip { - public ITooltip GetCustomTooltip() => new UserGridPanelTooltip(IsTooltipEnabled); + public ITooltip GetCustomTooltip() => new UserGridPanelTooltip(this); public UserGridPanel TooltipContent => new UserGridPanel(user!) { Width = 300 }; - public bool IsTooltipEnabled; + public override LocalisableString TooltipText + { + get + { + if (!Enabled.Value) + return string.Empty; + + return !IsTooltipEnabled ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile; + } + set => throw new NotSupportedException(); + } + + /// + /// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username. + /// Setting this to true exposes the username via tooltip for special cases where this is not true. + /// + // public bool ShowUsernameTooltip { get; set; } + + public bool IsTooltipEnabled { get; set; } private readonly APIUser? user; @@ -35,12 +56,16 @@ namespace osu.Game.Users.Drawables public ClickableAvatar(APIUser? user = null) { this.user = user; - IsTooltipEnabled = true; if (user?.Id != APIUser.SYSTEM_USER_ID) Action = openProfile; } + public void SetValue(out bool value) + { + value = IsTooltipEnabled; + } + [BackgroundDependencyLoader] private void load() { @@ -61,18 +86,22 @@ namespace osu.Game.Users.Drawables return base.OnClick(e); } - private partial class UserGridPanelTooltip : VisibilityContainer, ITooltip + public partial class UserGridPanelTooltip : VisibilityContainer, ITooltip { - private readonly bool isEnabled; + private readonly ClickableAvatar parent; private UserGridPanel? displayedUser; + private bool isEnabled; - public UserGridPanelTooltip(bool isEnabled = true) + public UserGridPanelTooltip(ClickableAvatar parent) { - this.isEnabled = isEnabled; + this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); + isEnabled = this.parent.IsTooltipEnabled; } protected override void PopIn() { + parent.SetValue(out isEnabled); + if (displayedUser is null || !isEnabled) { return; diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index a970997056..64d64c56ce 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -75,6 +75,7 @@ namespace osu.Game.Users.Drawables return new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, + IsTooltipEnabled = showUserPanel }; } else From f897c21b3f5b57b5892f25cef3644aabf82d7c43 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Mon, 6 Nov 2023 15:25:12 +0100 Subject: [PATCH 551/896] partial change --- osu.Game/Users/Drawables/ClickableAvatar.cs | 39 ++++++++------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index c11ad7f720..376ce0b821 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -15,14 +15,14 @@ using osuTK; namespace osu.Game.Users.Drawables { - public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip + public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip { - public ITooltip GetCustomTooltip() => new UserGridPanelTooltip(this); - - public UserGridPanel TooltipContent => new UserGridPanel(user!) + public ITooltip GetCustomTooltip() { - Width = 300 - }; + return new APIUserTooltip(user); + } + + public APIUser? TooltipContent => user; public override LocalisableString TooltipText { @@ -36,12 +36,6 @@ namespace osu.Game.Users.Drawables set => throw new NotSupportedException(); } - /// - /// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username. - /// Setting this to true exposes the username via tooltip for special cases where this is not true. - /// - // public bool ShowUsernameTooltip { get; set; } - public bool IsTooltipEnabled { get; set; } private readonly APIUser? user; @@ -86,28 +80,23 @@ namespace osu.Game.Users.Drawables return base.OnClick(e); } - public partial class UserGridPanelTooltip : VisibilityContainer, ITooltip + public partial class APIUserTooltip : VisibilityContainer, ITooltip { - private readonly ClickableAvatar parent; - private UserGridPanel? displayedUser; - private bool isEnabled; + private APIUser? user; - public UserGridPanelTooltip(ClickableAvatar parent) + public APIUserTooltip(APIUser? user) { - this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); - isEnabled = this.parent.IsTooltipEnabled; + this.user = user; } protected override void PopIn() { - parent.SetValue(out isEnabled); - - if (displayedUser is null || !isEnabled) + if (user is null) { return; } - Child = displayedUser; + Child = new UserGridPanel(user); this.FadeIn(20, Easing.OutQuint); } @@ -115,9 +104,9 @@ namespace osu.Game.Users.Drawables public void Move(Vector2 pos) => Position = pos; - public void SetContent(UserGridPanel userGridPanel) + public void SetContent(APIUser user) { - displayedUser = userGridPanel; + this.user = user; } } } From 915feeffb05389da36e9eb4e1c75f962202b3703 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 6 Nov 2023 17:37:25 +0300 Subject: [PATCH 552/896] Revert gameplay cursor scale changes --- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 12506c83b9..555610a3b6 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -13,16 +14,18 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI { public partial class OsuResumeOverlay : ResumeOverlay { - private Container resumeCursorContainer; + private Container cursorScaleContainer; private OsuClickToResumeCursor clickToResumeCursor; private OsuCursorContainer localCursorContainer; + private IBindable localCursorScale; public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; @@ -31,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load() { - Add(resumeCursorContainer = new Container + Add(cursorScaleContainer = new Container { Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume } }); @@ -42,11 +45,17 @@ namespace osu.Game.Rulesets.Osu.UI base.PopIn(); GameplayCursor.ActiveCursor.Hide(); - resumeCursorContainer.Position = ToLocalSpace(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre); + cursorScaleContainer.Position = ToLocalSpace(GameplayCursor.ActiveCursor.ScreenSpaceDrawQuad.Centre); clickToResumeCursor.Appear(); if (localCursorContainer == null) + { Add(localCursorContainer = new OsuCursorContainer()); + + localCursorScale = new BindableFloat(); + localCursorScale.BindTo(localCursorContainer.CursorScale); + localCursorScale.BindValueChanged(scale => cursorScaleContainer.Scale = new Vector2(scale.NewValue), true); + } } protected override void PopOut() From 75fbbb35ad2f88524670a19312dcb43569db8190 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 6 Nov 2023 18:28:51 +0300 Subject: [PATCH 553/896] Move cursor scale application within `OsuCursor` Doing so takes down two birds with one stone. 1. `ResumeOverlay` having to manually apply cursor scale to its "resume cursor". 2. Resume cursor input handling scaling up with the gameplay setting. Now, only the sprite itself gets scaled. --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 67 ++++++++++++++++--- .../UI/Cursor/OsuCursorContainer.cs | 61 ++--------------- osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs | 12 +--- 3 files changed, 68 insertions(+), 72 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 66c86ee09d..ab1bb0cf5a 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -4,12 +4,16 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -20,12 +24,29 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { private const float size = 28; + private const float pressed_scale = 1.2f; + private const float released_scale = 1f; + private bool cursorExpand; private SkinnableDrawable cursorSprite; + private Container cursorScaleContainer = null!; private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite; + public IBindable CursorScale => cursorScale; + + private readonly Bindable cursorScale = new BindableFloat(1); + + private Bindable userCursorScale = null!; + private Bindable autoCursorScale = null!; + + [Resolved(canBeNull: true)] + private GameplayState state { get; set; } + + [Resolved] + private OsuConfigManager config { get; set; } + public OsuCursor() { Origin = Anchor.Centre; @@ -33,15 +54,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Size = new Vector2(size); } - protected override void SkinChanged(ISkinSource skin) - { - cursorExpand = skin.GetConfig(OsuSkinConfiguration.CursorExpand)?.Value ?? true; - } - [BackgroundDependencyLoader] private void load() { - InternalChild = new Container + InternalChild = cursorScaleContainer = new Container { RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, @@ -52,10 +68,45 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Anchor = Anchor.Centre, } }; + + userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); + userCursorScale.ValueChanged += _ => calculateCursorScale(); + + autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); + autoCursorScale.ValueChanged += _ => calculateCursorScale(); + + cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true); } - private const float pressed_scale = 1.2f; - private const float released_scale = 1f; + protected override void LoadComplete() + { + base.LoadComplete(); + calculateCursorScale(); + } + + /// + /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size. + /// + public static float GetScaleForCircleSize(float circleSize) => + 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY; + + private void calculateCursorScale() + { + float scale = userCursorScale.Value; + + if (autoCursorScale.Value && state != null) + { + // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. + scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize); + } + + cursorScale.Value = scale; + } + + protected override void SkinChanged(ISkinSource skin) + { + cursorExpand = skin.GetConfig(OsuSkinConfiguration.CursorExpand)?.Value ?? true; + } public void Expand() { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index bf1ff872dd..ba8a634ff7 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -11,11 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; @@ -23,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { public partial class OsuCursorContainer : GameplayCursorContainer, IKeyBindingHandler { + public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor; + protected override Drawable CreateCursor() => new OsuCursor(); protected override Container Content => fadeContainer; @@ -33,13 +32,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly Drawable cursorTrail; - public IBindable CursorScale => cursorScale; - - private readonly Bindable cursorScale = new BindableFloat(1); - - private Bindable userCursorScale; - private Bindable autoCursorScale; - private readonly CursorRippleVisualiser rippleVisualiser; public OsuCursorContainer() @@ -56,12 +48,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor }; } - [Resolved(canBeNull: true)] - private GameplayState state { get; set; } - - [Resolved] - private OsuConfigManager config { get; set; } - [BackgroundDependencyLoader(true)] private void load(OsuRulesetConfigManager rulesetConfig) { @@ -74,46 +60,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor showTrail.BindValueChanged(v => cursorTrail.FadeTo(v.NewValue ? 1 : 0, 200), true); - userCursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); - userCursorScale.ValueChanged += _ => calculateScale(); - - autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); - autoCursorScale.ValueChanged += _ => calculateScale(); - - CursorScale.BindValueChanged(e => + ActiveCursor.CursorScale.BindValueChanged(e => { var newScale = new Vector2(e.NewValue); - ActiveCursor.Scale = newScale; rippleVisualiser.CursorScale = newScale; cursorTrail.Scale = newScale; }, true); - - calculateScale(); - } - - /// - /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size. - /// - public static float GetScaleForCircleSize(float circleSize) => - 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY; - - private void calculateScale() - { - float scale = userCursorScale.Value; - - if (autoCursorScale.Value && state != null) - { - // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. - scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize); - } - - cursorScale.Value = scale; - - var newScale = new Vector2(scale); - - ActiveCursor.ScaleTo(newScale, 400, Easing.OutQuint); - cursorTrail.Scale = newScale; } private int downCount; @@ -121,9 +74,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private void updateExpandedState() { if (downCount > 0) - (ActiveCursor as OsuCursor)?.Expand(); + ActiveCursor.Expand(); else - (ActiveCursor as OsuCursor)?.Contract(); + ActiveCursor.Contract(); } public bool OnPressed(KeyBindingPressEvent e) @@ -160,13 +113,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor protected override void PopIn() { fadeContainer.FadeTo(1, 300, Easing.OutQuint); - ActiveCursor.ScaleTo(CursorScale.Value, 400, Easing.OutQuint); + ActiveCursor.ScaleTo(1f, 400, Easing.OutQuint); } protected override void PopOut() { fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint); - ActiveCursor.ScaleTo(CursorScale.Value * 0.8f, 450, Easing.OutQuint); + ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint); } private partial class DefaultCursorTrail : CursorTrail diff --git a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs index 555610a3b6..ea49836772 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuResumeOverlay.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -14,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.UI @@ -25,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.UI private OsuClickToResumeCursor clickToResumeCursor; private OsuCursorContainer localCursorContainer; - private IBindable localCursorScale; public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null; @@ -49,13 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI clickToResumeCursor.Appear(); if (localCursorContainer == null) - { Add(localCursorContainer = new OsuCursorContainer()); - - localCursorScale = new BindableFloat(); - localCursorScale.BindTo(localCursorContainer.CursorScale); - localCursorScale.BindValueChanged(scale => cursorScaleContainer.Scale = new Vector2(scale.NewValue), true); - } } protected override void PopOut() @@ -98,7 +89,8 @@ namespace osu.Game.Rulesets.Osu.UI { case OsuAction.LeftButton: case OsuAction.RightButton: - if (!IsHovered) return false; + if (!IsHovered) + return false; this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); From e12ee29a942279becf7ca4fd3816af1787b3fce8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 6 Nov 2023 18:35:30 +0300 Subject: [PATCH 554/896] Update existing test coverage --- osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index c84a6ab70f..e6696032ae 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -94,16 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("load content", loadContent); - AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale); + AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == OsuCursor.GetScaleForCircleSize(circleSize) * userScale); AddStep("set user scale to 1", () => config.SetValue(OsuSetting.GameplayCursorSize, 1f)); - AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize)); + AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == OsuCursor.GetScaleForCircleSize(circleSize)); AddStep("turn off autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, false)); - AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1); + AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == 1); AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); - AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale); + AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == userScale); } [Test] From 073249dafb3605a36bb8482f73f2dac1b7733b9f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 6 Nov 2023 18:35:46 +0300 Subject: [PATCH 555/896] Allow tinkering with cursor-related settings in resume overlay test scene --- .../TestSceneResumeOverlay.cs | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 81dc64cda9..49a8254f53 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -2,11 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI; using osu.Game.Screens.Play; +using osu.Game.Tests.Gameplay; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests @@ -19,24 +22,37 @@ namespace osu.Game.Rulesets.Osu.Tests private bool resumeFired; - [SetUp] - public void SetUp() => Schedule(() => - { - Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) - { - Children = new Drawable[] - { - cursor = new CursorContainer(), - resume = new OsuResumeOverlay - { - GameplayCursor = cursor - }, - } - }; + private OsuConfigManager localConfig = null!; - resumeFired = false; - resume.ResumeAction = () => resumeFired = true; - }); + [Cached] + private GameplayState gameplayState; + + public TestSceneResumeOverlay() + { + gameplayState = TestGameplayState.Create(new OsuRuleset()); + } + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + AddSliderStep("cursor size", 0.1f, 2f, 1f, v => localConfig.SetValue(OsuSetting.GameplayCursorSize, v)); + AddSliderStep("circle size", 0f, 10f, 0f, val => + { + gameplayState.Beatmap.Difficulty.CircleSize = val; + SetUp(); + }); + + AddToggleStep("auto size", v => localConfig.SetValue(OsuSetting.AutoCursorSize, v)); + } + + [SetUp] + public void SetUp() => Schedule(loadContent); [Test] public void TestResume() @@ -53,6 +69,14 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden); } + private void loadContent() + { + Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) { Children = new Drawable[] { cursor = new CursorContainer(), resume = new OsuResumeOverlay { GameplayCursor = cursor }, } }; + + resumeFired = false; + resume.ResumeAction = () => resumeFired = true; + } + private partial class ManualOsuInputManager : OsuInputManager { public ManualOsuInputManager(RulesetInfo ruleset) From a136f272cf78ec3572bc13bdcf0de47b74789a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 6 Nov 2023 16:40:57 +0100 Subject: [PATCH 556/896] Just use yellow for system mods --- osu.Game/Graphics/OsuColour.cs | 2 +- osu.Game/Rulesets/UI/ModIcon.cs | 4 +--- osu.Game/Rulesets/UI/ModSwitchSmall.cs | 6 ++---- osu.Game/Rulesets/UI/ModSwitchTiny.cs | 4 +--- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 2e19eac572..a417164e27 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -162,7 +162,7 @@ namespace osu.Game.Graphics return Pink1; case ModType.System: - return Gray5; + return Yellow; default: throw new ArgumentOutOfRangeException(nameof(modType), modType, "Unknown mod type"); diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index d09db37f2a..d1776c5c0b 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -204,9 +204,7 @@ namespace osu.Game.Rulesets.UI private void updateColour() { - modAcronym.Colour = modIcon.Colour = mod.Type != ModType.System - ? OsuColour.Gray(84) - : colours.Yellow; + modAcronym.Colour = modIcon.Colour = OsuColour.Gray(84); extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour; extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f); diff --git a/osu.Game/Rulesets/UI/ModSwitchSmall.cs b/osu.Game/Rulesets/UI/ModSwitchSmall.cs index 452a5599ba..6e96cc8e6f 100644 --- a/osu.Game/Rulesets/UI/ModSwitchSmall.cs +++ b/osu.Game/Rulesets/UI/ModSwitchSmall.cs @@ -88,12 +88,10 @@ namespace osu.Game.Rulesets.UI var modTypeColour = colours.ForModType(mod.Type); inactiveForegroundColour = colourProvider?.Background5 ?? colours.Gray3; - activeForegroundColour = mod.Type != ModType.System ? modTypeColour : colours.Yellow; + activeForegroundColour = modTypeColour; inactiveBackgroundColour = colourProvider?.Background2 ?? colours.Gray5; - activeBackgroundColour = mod.Type != ModType.System - ? Interpolation.ValueAt(0.1f, Colour4.Black, modTypeColour, 0, 1) - : modTypeColour; + activeBackgroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, modTypeColour, 0, 1); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/UI/ModSwitchTiny.cs b/osu.Game/Rulesets/UI/ModSwitchTiny.cs index bb121c085c..4d50e702af 100644 --- a/osu.Game/Rulesets/UI/ModSwitchTiny.cs +++ b/osu.Game/Rulesets/UI/ModSwitchTiny.cs @@ -112,9 +112,7 @@ namespace osu.Game.Rulesets.UI activeBackgroundColour = modTypeColour; inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5; - activeForegroundColour = Mod.Type != ModType.System - ? Interpolation.ValueAt(0.1f, Colour4.Black, modTypeColour, 0, 1) - : colours.Yellow; + activeForegroundColour = Interpolation.ValueAt(0.1f, Colour4.Black, modTypeColour, 0, 1); } protected override void LoadComplete() From 9d10d93085cd3cde44d63836911bc607d5618901 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 6 Nov 2023 20:45:52 +0300 Subject: [PATCH 557/896] Adjust test scene to see graph flickering --- .../Visual/Online/TestSceneGraph.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs index 4f19003638..eee29e0aeb 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs @@ -13,22 +13,20 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public partial class TestSceneGraph : OsuTestScene { + private readonly BarGraph graph; + public TestSceneGraph() { - BarGraph graph; - - Children = new[] + Child = graph = new BarGraph { - graph = new BarGraph - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(0.5f), - }, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f), }; AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Select(i => (float)i)); + AddStep("small values", () => graph.Values = Enumerable.Range(1, 10).Select(i => i * 0.01f).Concat(new[] { 100f })); AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).Select(i => (float)i)); AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().Select(i => (float)i)); AddStep("empty values", () => graph.Values = Array.Empty()); @@ -37,5 +35,12 @@ namespace osu.Game.Tests.Visual.Online AddStep("Left to right", () => graph.Direction = BarDirection.LeftToRight); AddStep("Right to left", () => graph.Direction = BarDirection.RightToLeft); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + graph.MoveToY(-10, 1000).Then().MoveToY(10, 1000).Loop(); + } } } From 944fee56f8d8e61895882a5c55c9d6ff0e6c637e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 6 Nov 2023 21:48:47 +0300 Subject: [PATCH 558/896] Add failing test case --- .../TestSceneBeatmapEditorNavigation.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index d0fa5fc737..d9757d8584 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -6,11 +6,15 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; @@ -232,6 +236,35 @@ namespace osu.Game.Tests.Visual.Navigation () => Is.EqualTo(beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0).ID)); } + [Test] + public void TestCreateNewDifficultyOnNonExistentBeatmap() + { + AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); + + AddStep("open editor", () => Game.ChildrenOfType().Single().OnEdit.Invoke()); + AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); + AddStep("click on file", () => + { + var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "File"); + item.TriggerClick(); + }); + AddStep("click on create new difficulty", () => + { + var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "Create new difficulty"); + item.TriggerClick(); + }); + AddStep("click on catch", () => + { + var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "osu!catch"); + item.TriggerClick(); + }); + AddAssert("save dialog displayed", () => Game.ChildrenOfType().Single().CurrentDialog is PromptForSaveDialog); + + AddStep("press forget all changes", () => Game.ChildrenOfType().Single().CurrentDialog!.PerformAction()); + AddWaitStep("wait", 5); + AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits"); + } + private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; From b2749943e27ae31fb392b1bbad918cbd9432412e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 6 Nov 2023 21:52:46 +0300 Subject: [PATCH 559/896] Display "required save" popup when creating another difficulty on a new beatmap --- osu.Game/Screens/Edit/Editor.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 91c3c98f01..3136faf855 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1095,6 +1095,19 @@ namespace osu.Game.Screens.Edit protected void CreateNewDifficulty(RulesetInfo rulesetInfo) { + if (isNewBeatmap) + { + dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () => + { + if (!Save()) + return; + + CreateNewDifficulty(rulesetInfo); + })); + + return; + } + if (!rulesetInfo.Equals(editorBeatmap.BeatmapInfo.Ruleset)) { switchToNewDifficulty(rulesetInfo, false); From 38d16f620cdef1ab457ce2b82c5c9d1d91113761 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 6 Nov 2023 21:55:00 +0300 Subject: [PATCH 560/896] Alter test case to comply with new behaviour --- .../Visual/Navigation/TestSceneBeatmapEditorNavigation.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index d9757d8584..b79b61202b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -12,9 +12,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Overlays.Dialog; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; @@ -258,10 +256,10 @@ namespace osu.Game.Tests.Visual.Navigation var item = getEditor().ChildrenOfType().Single(i => i.Item.Text.Value.ToString() == "osu!catch"); item.TriggerClick(); }); - AddAssert("save dialog displayed", () => Game.ChildrenOfType().Single().CurrentDialog is PromptForSaveDialog); + AddAssert("save dialog displayed", () => Game.ChildrenOfType().Single().CurrentDialog is SaveRequiredPopupDialog); - AddStep("press forget all changes", () => Game.ChildrenOfType().Single().CurrentDialog!.PerformAction()); - AddWaitStep("wait", 5); + AddStep("press save", () => Game.ChildrenOfType().Single().CurrentDialog!.PerformOkAction()); + AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits"); } From d6e7145e1c676ff45eb820f42375500cba43c3a1 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Nov 2023 20:42:40 +0100 Subject: [PATCH 561/896] Add new setting for GameplayDisableTaps --- osu.Game/Configuration/OsuConfigManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index e3f950ce2c..21079fc092 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -108,6 +108,8 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.MouseDisableWheel, false); SetDefault(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay); + SetDefault(OsuSetting.GameplayDisableTaps, false); + // Graphics SetDefault(OsuSetting.ShowFpsDisplay, false); @@ -332,7 +334,7 @@ namespace osu.Game.Configuration FadePlayfieldWhenHealthLow, /// - /// Disables mouse buttons clicks and touchscreen taps during gameplay. + /// Disables mouse buttons clicks during gameplay. /// MouseDisableButtons, MouseDisableWheel, @@ -412,6 +414,7 @@ namespace osu.Game.Configuration EditorLimitedDistanceSnap, ReplaySettingsOverlay, AutomaticallyDownloadMissingBeatmaps, - EditorShowSpeedChanges + EditorShowSpeedChanges, + GameplayDisableTaps, } } From c1967a5cbb640cf296832efec00b392922c398c9 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Nov 2023 20:43:24 +0100 Subject: [PATCH 562/896] Make tests fail --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index 2e62689e2c..19340aac15 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -133,8 +133,11 @@ namespace osu.Game.Rulesets.Osu.Tests } [Test] - public void TestSimpleInput() + public void TestSimpleInput([Values] bool disableMouseButtons) { + // OsuSetting.MouseDisableButtons should not affect touch taps + AddStep($"{(disableMouseButtons ? "disable" : "enable")} mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, disableMouseButtons)); + beginTouch(TouchSource.Touch1); assertKeyCounter(1, 0); @@ -468,7 +471,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestInputWhileMouseButtonsDisabled() { - AddStep("Disable mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, true)); + AddStep("Disable gameplay taps", () => config.SetValue(OsuSetting.GameplayDisableTaps, true)); beginTouch(TouchSource.Touch1); @@ -620,6 +623,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Release all touches", () => { config.SetValue(OsuSetting.MouseDisableButtons, false); + config.SetValue(OsuSetting.GameplayDisableTaps, false); foreach (TouchSource source in InputManager.CurrentState.Touch.ActiveSources) InputManager.EndTouch(new Touch(source, osuInputManager.ScreenSpaceDrawQuad.Centre)); }); From ea357bafddd9970fb2c7f06686e1604b6caca76f Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Nov 2023 20:53:22 +0100 Subject: [PATCH 563/896] Fix tests by using the correct setting for touch input --- osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs | 8 +++----- osu.Game/Rulesets/UI/RulesetInputManager.cs | 6 ++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs index 5277a1f7d6..994ec024b1 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly OsuInputManager osuInputManager; - private Bindable mouseDisabled = null!; + private Bindable tapsDisabled = null!; public OsuTouchInputMapper(OsuInputManager inputManager) { @@ -43,9 +43,7 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - // The mouse button disable setting affects touch. It's a bit weird. - // This is mostly just doing the same as what is done in RulesetInputManager to match behaviour. - mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); + tapsDisabled = config.GetBindable(OsuSetting.GameplayDisableTaps); } // Required to handle touches outside of the playfield when screen scaling is enabled. @@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.UI : OsuAction.LeftButton; // Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future. - bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action); + bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !tapsDisabled.Value && trackedTouches.All(t => t.Action != action); // If we can actually accept as an action, check whether this tap was on a circle's receptor. // This case gets special handling to allow for empty-space stream tapping. diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 39b83ecca1..eb19368fc8 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.UI private void load(OsuConfigManager config) { mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); + tapsDisabled = config.GetBindable(OsuSetting.GameplayDisableTaps); } #region Action mapping (for replays) @@ -124,6 +125,7 @@ namespace osu.Game.Rulesets.UI #region Setting application (disables etc.) private Bindable mouseDisabled; + private Bindable tapsDisabled; protected override bool Handle(UIEvent e) { @@ -147,9 +149,9 @@ namespace osu.Game.Rulesets.UI protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e) { - if (mouseDisabled.Value) + if (tapsDisabled.Value) { - // Only propagate positional data when mouse buttons are disabled. + // Only propagate positional data when taps are disabled. e = new TouchStateChangeEvent(e.State, e.Input, e.Touch, false, e.LastPosition); } From f8b5ecc92a2e303a87a23c218a24c103881cb8c5 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Nov 2023 21:07:15 +0100 Subject: [PATCH 564/896] Update UI to use the new setting --- osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs | 2 +- osu.Game/Screens/Play/PlayerSettings/InputSettings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs index b1b1b59429..793b707bfc 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Add(new SettingsCheckbox { LabelText = TouchSettingsStrings.DisableTapsDuringGameplay, - Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) + Current = osuConfig.GetBindable(OsuSetting.GameplayDisableTaps) }); } diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index f6b0cddcf1..96b543d176 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -27,6 +27,6 @@ namespace osu.Game.Screens.Play.PlayerSettings } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) => mouseButtonsCheckbox.Current = config.GetBindable(OsuSetting.MouseDisableButtons); + private void load(OsuConfigManager config) => mouseButtonsCheckbox.Current = config.GetBindable(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.GameplayDisableTaps); } } From a4ac50cf86f8cbd148eedf75b765c65ad34301b6 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 6 Nov 2023 21:10:04 +0100 Subject: [PATCH 565/896] Revert "Rename popup/binding string to `Toggle gameplay clicks/taps`" This reverts commit 0d8bfedf5d3693809e76471b9feb47ebf140e40b. --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- osu.Game/Localisation/GlobalActionKeyBindingStrings.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 21079fc092..c44a089c49 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -248,7 +248,7 @@ namespace osu.Game.Configuration ), new TrackedSetting(OsuSetting.MouseDisableButtons, disabledState => new SettingDescription( rawValue: !disabledState, - name: GlobalActionKeyBindingStrings.ToggleGameplayClicksTaps, + name: GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons, value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(), shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons)) ), diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index b8163cc3b1..947cd5f54f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -237,7 +237,7 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.TakeScreenshot))] TakeScreenshot, - [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleGameplayClicksTaps))] + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons))] ToggleGameplayMouseButtons, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.Back))] diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 1bbbbdc3bc..8356c480dd 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -70,9 +70,9 @@ namespace osu.Game.Localisation public static LocalisableString TakeScreenshot => new TranslatableString(getKey(@"take_screenshot"), @"Take screenshot"); /// - /// "Toggle gameplay clicks/taps" + /// "Toggle gameplay mouse buttons" /// - public static LocalisableString ToggleGameplayClicksTaps => new TranslatableString(getKey(@"toggle_gameplay_clicks_taps"), @"Toggle gameplay clicks/taps"); + public static LocalisableString ToggleGameplayMouseButtons => new TranslatableString(getKey(@"toggle_gameplay_mouse_buttons"), @"Toggle gameplay mouse buttons"); /// /// "Back" From 01e59d134a9b8436404a1c6de4bc5cb07332dfe8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 7 Nov 2023 00:48:51 +0300 Subject: [PATCH 566/896] Adjust health bar settings on default components initialiser to match new layout --- osu.Game/Skinning/ArgonSkin.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 3262812d24..6c4074fd92 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -128,9 +128,11 @@ namespace osu.Game.Skinning // elements default to beneath the health bar const float components_x_offset = 50; - health.Anchor = Anchor.TopCentre; - health.Origin = Anchor.TopCentre; - health.Y = 15; + health.Anchor = Anchor.TopLeft; + health.Origin = Anchor.TopLeft; + health.BarLength.Value = 0.22f; + health.BarHeight.Value = 30f; + health.Position = new Vector2(components_x_offset, 20f); if (scoreWedge != null) { From 754e05213c452f1f1ddc1c3485ae9e4270b23979 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 7 Nov 2023 00:53:20 +0300 Subject: [PATCH 567/896] Update argon score wedge design --- osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs | 43 -------------------- osu.Game/Skinning/ArgonSkin.cs | 2 +- 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs b/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs index dc0130fb8e..fe495b421f 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs @@ -5,22 +5,13 @@ 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.Shapes; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { public partial class ArgonScoreWedge : CompositeDrawable, ISerialisableDrawable { - private SliderPath barPath = null!; - - private const float main_path_radius = 1f; - public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] @@ -28,30 +19,7 @@ namespace osu.Game.Screens.Play.HUD { AutoSizeAxes = Axes.Both; - const float bar_length = 430 - main_path_radius * 2; - const float bar_top = 0; - const float bar_bottom = 80; - const float curve_start = bar_length - 105; - const float curve_end = bar_length - 35; - - const float curve_smoothness = 10; - - Vector2 diagonalDir = (new Vector2(curve_end, bar_top) - new Vector2(curve_start, bar_bottom)).Normalized(); - - barPath = new SliderPath(new[] - { - new PathControlPoint(new Vector2(0, bar_bottom), PathType.Linear), - new PathControlPoint(new Vector2(curve_start - curve_smoothness, bar_bottom), PathType.Bezier), - new PathControlPoint(new Vector2(curve_start, bar_bottom)), - new PathControlPoint(new Vector2(curve_start, bar_bottom) + diagonalDir * curve_smoothness, PathType.Linear), - new PathControlPoint(new Vector2(curve_end, bar_top) - diagonalDir * curve_smoothness, PathType.Bezier), - new PathControlPoint(new Vector2(curve_end, bar_top)), - new PathControlPoint(new Vector2(curve_end + curve_smoothness, bar_top), PathType.Linear), - new PathControlPoint(new Vector2(bar_length, bar_top)), - }); - var vertices = new List(); - barPath.GetPathToProgress(vertices, 0, 1); InternalChildren = new Drawable[] { @@ -66,17 +34,6 @@ namespace osu.Game.Screens.Play.HUD WedgeHeight = { Value = 72 }, Position = new Vector2(4, 5) }, - new SmoothPath - { - Colour = Color4.White, - PathRadius = 1f, - Vertices = vertices, - }, - new Circle - { - Y = bar_bottom - 1.5f + main_path_radius, - Size = new Vector2(300f, 3f), - } }; } } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 6c4074fd92..42ab93d3c5 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -136,7 +136,7 @@ namespace osu.Game.Skinning if (scoreWedge != null) { - scoreWedge.Position = new Vector2(-50, 50); + scoreWedge.Position = new Vector2(-50, 15); if (score != null) score.Position = new Vector2(components_x_offset, scoreWedge.Y + 15); From 4c7db4c2625483d557f1e4b6964579adcb7a0060 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 7 Nov 2023 00:49:22 +0300 Subject: [PATCH 568/896] Make score counter right-aligned --- osu.Game/Skinning/ArgonSkin.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 42ab93d3c5..e00973f710 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -139,7 +139,10 @@ namespace osu.Game.Skinning scoreWedge.Position = new Vector2(-50, 15); if (score != null) - score.Position = new Vector2(components_x_offset, scoreWedge.Y + 15); + { + score.Origin = Anchor.TopRight; + score.Position = new Vector2(components_x_offset + 200, scoreWedge.Y + 30); + } if (accuracy != null) { From ce36884ef05dbe1664e93ef9285abb43b0849067 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 7 Nov 2023 00:54:34 +0300 Subject: [PATCH 569/896] Make score wireframes display up to required digits count --- .../Screens/Play/HUD/ArgonScoreCounter.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 03635f2914..5d40dd81a3 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -1,9 +1,11 @@ // 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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -19,7 +21,7 @@ namespace osu.Game.Screens.Play.HUD public partial class ArgonScoreCounter : GameplayScoreCounter, ISerialisableDrawable { [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] - public BindableFloat WireframeOpactiy { get; } = new BindableFloat(0.4f) + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) { Precision = 0.01f, MinValue = 0, @@ -28,9 +30,12 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } + protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(); + protected override IHasText CreateText() => new ArgonScoreTextComponent { - WireframeOpactiy = { BindTarget = WireframeOpactiy }, + RequiredDisplayDigits = { BindTarget = RequiredDisplayDigits }, + WireframeOpacity = { BindTarget = WireframeOpacity }, }; private partial class ArgonScoreTextComponent : CompositeDrawable, IHasText @@ -38,14 +43,15 @@ namespace osu.Game.Screens.Play.HUD private readonly ArgonScoreSpriteText wireframesPart; private readonly ArgonScoreSpriteText textPart; - public IBindable WireframeOpactiy { get; } = new BindableFloat(); + public IBindable RequiredDisplayDigits { get; } = new BindableInt(); + public IBindable WireframeOpacity { get; } = new BindableFloat(); public LocalisableString Text { get => textPart.Text; set { - wireframesPart.Text = value; + wireframesPart.Text = new string('#', Math.Max(value.ToString().Length, RequiredDisplayDigits.Value)); textPart.Text = value; } } @@ -56,15 +62,23 @@ namespace osu.Game.Screens.Play.HUD InternalChildren = new[] { - wireframesPart = new ArgonScoreSpriteText(@"wireframes"), - textPart = new ArgonScoreSpriteText(), + wireframesPart = new ArgonScoreSpriteText(@"wireframes") + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + textPart = new ArgonScoreSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, }; } protected override void LoadComplete() { base.LoadComplete(); - WireframeOpactiy.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true); + WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true); } private partial class ArgonScoreSpriteText : OsuSpriteText From 7c1c62ba8aef898bddaa6c07124a1053251bdf8a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 7 Nov 2023 01:58:26 +0300 Subject: [PATCH 570/896] Remove argon combo wedge and update combo counter position --- osu.Game/Screens/Play/HUD/ArgonComboWedge.cs | 27 -------------------- osu.Game/Skinning/ArgonSkin.cs | 21 +++++---------- 2 files changed, 7 insertions(+), 41 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/ArgonComboWedge.cs diff --git a/osu.Game/Screens/Play/HUD/ArgonComboWedge.cs b/osu.Game/Screens/Play/HUD/ArgonComboWedge.cs deleted file mode 100644 index 6da3727505..0000000000 --- a/osu.Game/Screens/Play/HUD/ArgonComboWedge.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.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - public partial class ArgonComboWedge : CompositeDrawable, ISerialisableDrawable - { - public bool UsesFixedAnchor { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - AutoSizeAxes = Axes.Both; - - InternalChild = new ArgonWedgePiece - { - WedgeWidth = { Value = 186 }, - WedgeHeight = { Value = 33 }, - }; - } - } -} diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index e00973f710..82c150ced7 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -118,7 +118,6 @@ namespace osu.Game.Skinning var scoreWedge = container.OfType().FirstOrDefault(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); - var comboWedge = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); var songProgress = container.OfType().FirstOrDefault(); var keyCounter = container.OfType().FirstOrDefault(); @@ -153,18 +152,6 @@ namespace osu.Game.Skinning } } - if (comboWedge != null) - { - comboWedge.Position = new Vector2(-12, 130); - - if (combo != null) - { - combo.Anchor = Anchor.TopLeft; - combo.Origin = Anchor.TopLeft; - combo.Position = new Vector2(components_x_offset, comboWedge.Y - 2); - } - } - var hitError = container.OfType().FirstOrDefault(); if (hitError != null) @@ -199,6 +186,13 @@ namespace osu.Game.Skinning keyCounter.Origin = Anchor.BottomRight; keyCounter.Position = new Vector2(-(hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); } + + if (combo != null && hitError != null) + { + combo.Anchor = Anchor.BottomLeft; + combo.Origin = Anchor.BottomLeft; + combo.Position = new Vector2(hitError.Width + padding, -50); + } } } }) @@ -209,7 +203,6 @@ namespace osu.Game.Skinning new ArgonScoreWedge(), new ArgonScoreCounter(), new ArgonAccuracyCounter(), - new ArgonComboWedge(), new ArgonComboCounter(), new BarHitErrorMeter(), new BarHitErrorMeter(), From 0dbba13686388559a25558ebbd4a4fd17c858e1d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 7 Nov 2023 01:59:00 +0300 Subject: [PATCH 571/896] Split argon score sprite text and update combo counter design --- .../Screens/Play/HUD/ArgonComboCounter.cs | 20 ++- .../Play/HUD/ArgonCounterTextComponent.cs | 152 ++++++++++++++++++ .../Screens/Play/HUD/ArgonScoreCounter.cs | 106 +----------- 3 files changed, 167 insertions(+), 111 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index 28c97c53aa..6a7d5ff665 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -2,28 +2,36 @@ // 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.Sprites; using osu.Framework.Localisation; -using osu.Game.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { public partial class ArgonComboCounter : ComboCounter { + [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) + { + Precision = 0.01f, + MinValue = 0, + MaxValue = 1, + }; + [BackgroundDependencyLoader] private void load(ScoreProcessor scoreProcessor) { Current.BindTo(scoreProcessor.Combo); } - protected override OsuSpriteText CreateSpriteText() - => base.CreateSpriteText().With(s => s.Font = FontUsage.Default.With(size: 36f)); + protected override LocalisableString FormatCount(int count) => $@"{count}x"; - protected override LocalisableString FormatCount(int count) + protected override IHasText CreateText() => new ArgonCounterTextComponent(Anchor.TopLeft, "COMBO") { - return $@"{count}x"; - } + WireframeOpacity = { BindTarget = WireframeOpacity }, + }; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs new file mode 100644 index 0000000000..9545168a46 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -0,0 +1,152 @@ +// 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.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Framework.Text; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonCounterTextComponent : CompositeDrawable, IHasText + { + private readonly LocalisableString? label; + + private readonly ArgonCounterSpriteText wireframesPart; + private readonly ArgonCounterSpriteText textPart; + + public IBindable RequiredDisplayDigits { get; } = new BindableInt(); + public IBindable WireframeOpacity { get; } = new BindableFloat(); + + public LocalisableString Text + { + get => textPart.Text; + set + { + wireframesPart.Text = new string('#', Math.Max(value.ToString().Count(char.IsDigit), RequiredDisplayDigits.Value)); + textPart.Text = value; + } + } + + public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null) + { + Anchor = anchor; + Origin = anchor; + + this.label = label; + + wireframesPart = new ArgonCounterSpriteText(@"wireframes") + { + Anchor = anchor, + Origin = anchor, + }; + textPart = new ArgonCounterSpriteText + { + Anchor = anchor, + Origin = anchor, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Alpha = label != null ? 1 : 0, + Text = label.GetValueOrDefault(), + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), + Colour = colours.Blue0, + Margin = new MarginPadding { Left = 2.5f }, + }, + new Container + { + AutoSizeAxes = Axes.Both, + Children = new[] + { + wireframesPart, + textPart, + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true); + } + + private partial class ArgonCounterSpriteText : OsuSpriteText + { + private readonly string? glyphLookupOverride; + + private GlyphStore glyphStore = null!; + + protected override char FixedWidthReferenceCharacter => '5'; + + public ArgonCounterSpriteText(string? glyphLookupOverride = null) + { + this.glyphLookupOverride = glyphLookupOverride; + + Shadow = false; + UseFullGlyphHeight = false; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + // todo: rename font + Font = new FontUsage(@"argon-score", 1, fixedWidth: true); + Spacing = new Vector2(-2, 0); + + glyphStore = new GlyphStore(skin, glyphLookupOverride); + } + + protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); + + private class GlyphStore : ITexturedGlyphLookupStore + { + private readonly ISkin skin; + private readonly string? glyphLookupOverride; + + public GlyphStore(ISkin skin, string? glyphLookupOverride) + { + this.skin = skin; + this.glyphLookupOverride = glyphLookupOverride; + } + + public ITexturedCharacterGlyph? Get(string fontName, char character) + { + string lookup = glyphLookupOverride ?? character.ToString(); + var texture = skin.GetTexture($"{fontName}-{lookup}"); + + if (texture == null) + return null; + + return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f); + } + + public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 5d40dd81a3..636565f181 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -1,20 +1,13 @@ // 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.Threading.Tasks; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; -using osu.Framework.Text; using osu.Game.Configuration; -using osu.Game.Graphics.Sprites; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -32,107 +25,10 @@ namespace osu.Game.Screens.Play.HUD protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(); - protected override IHasText CreateText() => new ArgonScoreTextComponent + protected override IHasText CreateText() => new ArgonCounterTextComponent(Anchor.TopRight) { RequiredDisplayDigits = { BindTarget = RequiredDisplayDigits }, WireframeOpacity = { BindTarget = WireframeOpacity }, }; - - private partial class ArgonScoreTextComponent : CompositeDrawable, IHasText - { - private readonly ArgonScoreSpriteText wireframesPart; - private readonly ArgonScoreSpriteText textPart; - - public IBindable RequiredDisplayDigits { get; } = new BindableInt(); - public IBindable WireframeOpacity { get; } = new BindableFloat(); - - public LocalisableString Text - { - get => textPart.Text; - set - { - wireframesPart.Text = new string('#', Math.Max(value.ToString().Length, RequiredDisplayDigits.Value)); - textPart.Text = value; - } - } - - public ArgonScoreTextComponent() - { - AutoSizeAxes = Axes.Both; - - InternalChildren = new[] - { - wireframesPart = new ArgonScoreSpriteText(@"wireframes") - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }, - textPart = new ArgonScoreSpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true); - } - - private partial class ArgonScoreSpriteText : OsuSpriteText - { - private readonly string? glyphLookupOverride; - - private GlyphStore glyphStore = null!; - - protected override char FixedWidthReferenceCharacter => '5'; - - public ArgonScoreSpriteText(string? glyphLookupOverride = null) - { - this.glyphLookupOverride = glyphLookupOverride; - - Shadow = false; - UseFullGlyphHeight = false; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin) - { - Font = new FontUsage(@"argon-score", 1, fixedWidth: true); - Spacing = new Vector2(-2, 0); - - glyphStore = new GlyphStore(skin, glyphLookupOverride); - } - - protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); - - private class GlyphStore : ITexturedGlyphLookupStore - { - private readonly ISkin skin; - private readonly string? glyphLookupOverride; - - public GlyphStore(ISkin skin, string? glyphLookupOverride) - { - this.skin = skin; - this.glyphLookupOverride = glyphLookupOverride; - } - - public ITexturedCharacterGlyph? Get(string fontName, char character) - { - string lookup = glyphLookupOverride ?? character.ToString(); - var texture = skin.GetTexture($"{fontName}-{lookup}"); - - if (texture == null) - return null; - - return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f); - } - - public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); - } - } - } } } From 05d941871860fc94db9d92d722d44eb4f190766a Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 7 Nov 2023 00:13:46 +0100 Subject: [PATCH 572/896] Rename setting to `TouchDisableGameplayTaps` for better visibility when searching --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs | 4 ++-- osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs | 2 +- osu.Game/Configuration/OsuConfigManager.cs | 4 ++-- osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs | 2 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 2 +- osu.Game/Screens/Play/PlayerSettings/InputSettings.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs index 19340aac15..25fe8170b1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuTouchInput.cs @@ -471,7 +471,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestInputWhileMouseButtonsDisabled() { - AddStep("Disable gameplay taps", () => config.SetValue(OsuSetting.GameplayDisableTaps, true)); + AddStep("Disable gameplay taps", () => config.SetValue(OsuSetting.TouchDisableGameplayTaps, true)); beginTouch(TouchSource.Touch1); @@ -623,7 +623,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Release all touches", () => { config.SetValue(OsuSetting.MouseDisableButtons, false); - config.SetValue(OsuSetting.GameplayDisableTaps, false); + config.SetValue(OsuSetting.TouchDisableGameplayTaps, false); foreach (TouchSource source in InputManager.CurrentState.Touch.ActiveSources) InputManager.EndTouch(new Touch(source, osuInputManager.ScreenSpaceDrawQuad.Centre)); }); diff --git a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs index 994ec024b1..e815d7873e 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuTouchInputMapper.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - tapsDisabled = config.GetBindable(OsuSetting.GameplayDisableTaps); + tapsDisabled = config.GetBindable(OsuSetting.TouchDisableGameplayTaps); } // Required to handle touches outside of the playfield when screen scaling is enabled. diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c44a089c49..6ef55ab919 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -108,7 +108,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.MouseDisableWheel, false); SetDefault(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay); - SetDefault(OsuSetting.GameplayDisableTaps, false); + SetDefault(OsuSetting.TouchDisableGameplayTaps, false); // Graphics SetDefault(OsuSetting.ShowFpsDisplay, false); @@ -415,6 +415,6 @@ namespace osu.Game.Configuration ReplaySettingsOverlay, AutomaticallyDownloadMissingBeatmaps, EditorShowSpeedChanges, - GameplayDisableTaps, + TouchDisableGameplayTaps, } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs index 793b707bfc..0056de6674 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Add(new SettingsCheckbox { LabelText = TouchSettingsStrings.DisableTapsDuringGameplay, - Current = osuConfig.GetBindable(OsuSetting.GameplayDisableTaps) + Current = osuConfig.GetBindable(OsuSetting.TouchDisableGameplayTaps) }); } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index eb19368fc8..35d05b87c0 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.UI private void load(OsuConfigManager config) { mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); - tapsDisabled = config.GetBindable(OsuSetting.GameplayDisableTaps); + tapsDisabled = config.GetBindable(OsuSetting.TouchDisableGameplayTaps); } #region Action mapping (for replays) diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 96b543d176..8a6e2759e3 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -27,6 +27,6 @@ namespace osu.Game.Screens.Play.PlayerSettings } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) => mouseButtonsCheckbox.Current = config.GetBindable(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.GameplayDisableTaps); + private void load(OsuConfigManager config) => mouseButtonsCheckbox.Current = config.GetBindable(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.DisableTapsDuringGameplay); } } From 7385c3c97bb48b507127dd8709d658788adbaf78 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 7 Nov 2023 00:17:15 +0100 Subject: [PATCH 573/896] Move `InputSettings` children creation code to BDL - Avoids now obsolete variable name - Makes changing to touch detection easier (access to session statics in BDL) --- .../Play/PlayerSettings/InputSettings.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 8a6e2759e3..1387e01305 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -11,22 +11,23 @@ namespace osu.Game.Screens.Play.PlayerSettings { public partial class InputSettings : PlayerSettingsGroup { - private readonly PlayerCheckbox mouseButtonsCheckbox; - public InputSettings() : base("Input Settings") { - Children = new Drawable[] - { - mouseButtonsCheckbox = new PlayerCheckbox - { - // TODO: change to touchscreen detection once https://github.com/ppy/osu/pull/25348 makes it in - LabelText = RuntimeInfo.IsDesktop ? MouseSettingsStrings.DisableClicksDuringGameplay : TouchSettingsStrings.DisableTapsDuringGameplay - } - }; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) => mouseButtonsCheckbox.Current = config.GetBindable(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.DisableTapsDuringGameplay); + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new PlayerCheckbox + { + // TODO: change to touchscreen detection once https://github.com/ppy/osu/pull/25348 makes it in + LabelText = RuntimeInfo.IsDesktop ? MouseSettingsStrings.DisableClicksDuringGameplay : TouchSettingsStrings.DisableTapsDuringGameplay, + Current = config.GetBindable(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.TouchDisableGameplayTaps) + } + }; + } } } From 8e8a88cfaf4852d4e80611677cb5948806096704 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Tue, 7 Nov 2023 00:54:15 +0100 Subject: [PATCH 574/896] Removed nullable line from Test --- osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index f65aff922e..77dcbf069b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; using NUnit.Framework; From 0834b79cc7fcb78873e381754f709deba0be4693 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Tue, 7 Nov 2023 00:56:24 +0100 Subject: [PATCH 575/896] Renamed method and moved Notifications inside --- osu.Game/OsuGame.cs | 15 ++++++--------- osu.Game/Screens/Edit/Editor.cs | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a9d4927e33..be1776a330 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -562,18 +562,15 @@ namespace osu.Game { if (ScreenStack.CurrentScreen is not Editor editor) { - postNotification(EditorStrings.MustBeInEdit); + Schedule(() => Notifications.Post(new SimpleNotification + { + Icon = FontAwesome.Solid.ExclamationTriangle, + Text = EditorStrings.MustBeInEdit + })); return; } - editor.SeekAndSelectHitObjects(timestamp, onError: postNotification); - return; - - void postNotification(LocalisableString message) => Schedule(() => Notifications.Post(new SimpleNotification - { - Icon = FontAwesome.Solid.ExclamationTriangle, - Text = message - })); + editor.HandleTimestamp(timestamp); } /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 60d26d9ec0..9e0671e91d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1,4 +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. #nullable disable @@ -1138,13 +1138,17 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } - public void SeekAndSelectHitObjects(string timestamp, Action onError) + public void HandleTimestamp(string timestamp) { string[] groups = EditorTimestampParser.GetRegexGroups(timestamp); if (groups.Length != 2 || string.IsNullOrEmpty(groups[0])) { - onError.Invoke(EditorStrings.FailedToProcessTimestamp); + Schedule(() => notifications.Post(new SimpleNotification + { + Icon = FontAwesome.Solid.ExclamationTriangle, + Text = EditorStrings.FailedToProcessTimestamp + })); return; } @@ -1156,7 +1160,11 @@ namespace osu.Game.Screens.Edit // Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues if (timeMinutes.Length > 5 || double.Parse(timeMinutes) > 30_000) { - onError.Invoke(EditorStrings.TooLongTimestamp); + Schedule(() => notifications.Post(new SimpleNotification + { + Icon = FontAwesome.Solid.ExclamationTriangle, + Text = EditorStrings.TooLongTimestamp + })); return; } From 44f127c8a86c9fa381118571c5af5e26f8cf141d Mon Sep 17 00:00:00 2001 From: ratinfx Date: Tue, 7 Nov 2023 01:02:45 +0100 Subject: [PATCH 576/896] Renamed method and made private --- .../Edit/Compose/Components/EditorBlueprintContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 60959ca27a..b68a690097 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // This makes sure HitObjects will have active Blueprints ready to display // after clicking on an Editor Timestamp/Link - Beatmap.SelectedHitObjects.CollectionChanged += SetHitObjectsAlive; + Beatmap.SelectedHitObjects.CollectionChanged += keepHitObjectsAlive; if (Composer != null) { @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray()); } - protected void SetHitObjectsAlive(object sender, NotifyCollectionChangedEventArgs e) + private void keepHitObjectsAlive(object sender, NotifyCollectionChangedEventArgs e) { if (e == null || e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null) return; @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Beatmap.HitObjectAdded -= AddBlueprintFor; Beatmap.HitObjectRemoved -= RemoveBlueprintFor; - Beatmap.SelectedHitObjects.CollectionChanged -= SetHitObjectsAlive; + Beatmap.SelectedHitObjects.CollectionChanged -= keepHitObjectsAlive; } usageEventBuffer?.Dispose(); From aa87e0a44d469bbdfb9abe9bca13ea0323eda774 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Tue, 7 Nov 2023 01:36:58 +0100 Subject: [PATCH 577/896] HitObject Selection logic and separation for gamemodes + moved time_regex into EditorTimestampParser --- .../Edit/ManiaHitObjectComposer.cs | 19 ++++++- .../Edit/OsuHitObjectComposer.cs | 13 ++++- .../Editing/TestSceneOpenEditorTimestamp.cs | 30 ++++------- osu.Game/Online/Chat/MessageFormatter.cs | 7 +-- osu.Game/OsuGame.cs | 6 +-- .../Edit/EditorTimestampParser.cs | 54 +++++-------------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 13 +++++ osu.Game/Screens/Edit/Editor.cs | 50 ++++++++--------- 8 files changed, 95 insertions(+), 97 deletions(-) rename osu.Game/{Screens => Rulesets}/Edit/EditorTimestampParser.cs (50%) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index b9db4168f4..d217f04651 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -11,6 +12,7 @@ using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -49,6 +51,21 @@ namespace osu.Game.Rulesets.Mania.Edit }; public override string ConvertSelectionToString() - => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); + => string.Join(ObjectSeparator, EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); + + public override bool HandleHitObjectSelection(HitObject hitObject, string objectInfo) + { + if (hitObject is not ManiaHitObject maniaHitObject) + return false; + + double[] split = objectInfo.Split('|').Select(double.Parse).ToArray(); + if (split.Length != 2) + return false; + + double timeValue = split[0]; + double columnValue = split[1]; + return Math.Abs(maniaHitObject.StartTime - timeValue) < 0.5 + && Math.Abs(maniaHitObject.Column - columnValue) < 0.5; + } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0f8c960b65..0c63cf71d8 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -104,7 +104,18 @@ namespace osu.Game.Rulesets.Osu.Edit => new OsuBlueprintContainer(this); public override string ConvertSelectionToString() - => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + => string.Join(ObjectSeparator, selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + + public override bool HandleHitObjectSelection(HitObject hitObject, string objectInfo) + { + if (hitObject is not OsuHitObject osuHitObject) + return false; + + if (!int.TryParse(objectInfo, out int comboValue) || comboValue < 1) + return false; + + return osuHitObject.IndexInCurrentCombo + 1 == comboValue; + } private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index 77dcbf069b..f7b976702a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -29,11 +29,14 @@ namespace osu.Game.Tests.Visual.Editing protected EditorBeatmap EditorBeatmap => Editor.ChildrenOfType().Single(); protected EditorClock EditorClock => Editor.ChildrenOfType().Single(); - protected void AddStepClickLink(string timestamp, string step = "") + protected void AddStepClickLink(string timestamp, string step = "", bool waitForSeek = true) { AddStep($"{step} {timestamp}", () => Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp)) ); + + if (waitForSeek) + AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); } protected void AddStepScreenModeTo(EditorScreenMode screenMode) @@ -76,7 +79,7 @@ namespace osu.Game.Tests.Visual.Editing .Any(); } - private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)> columnPairs = null) + private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null) { bool checkColumns = columnPairs != null ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) @@ -123,7 +126,7 @@ namespace osu.Game.Tests.Visual.Editing { RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; - AddStepClickLink("00:00:000"); + AddStepClickLink("00:00:000", waitForSeek: false); AddAssert("recieved 'must be in edit'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 1 ); @@ -131,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); - AddStepClickLink("00:00:000 (1)"); + AddStepClickLink("00:00:000 (1)", waitForSeek: false); AddAssert("recieved 'must be in edit'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 2 ); @@ -139,27 +142,12 @@ namespace osu.Game.Tests.Visual.Editing SetUpEditor(rulesetInfo); AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - AddStepClickLink("00:000", "invalid link"); + AddStepClickLink("00:000", "invalid link", waitForSeek: false); AddAssert("recieved 'failed to process'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 1 ); - AddStepClickLink("00:00:00:000", "invalid link"); - AddAssert("recieved 'failed to process'", () => - Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 2 - ); - - AddStepClickLink("00:00:000 ()", "invalid link"); - AddAssert("recieved 'failed to process'", () => - Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 3 - ); - - AddStepClickLink("00:00:000 (-1)", "invalid link"); - AddAssert("recieved 'failed to process'", () => - Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 4 - ); - - AddStepClickLink("50000:00:000", "too long link"); + AddStepClickLink("50000:00:000", "too long link", waitForSeek: false); AddAssert("recieved 'too long'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.TooLongTimestamp) == 1 ); diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 667175117f..9a194dba47 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets.Edit; namespace osu.Game.Online.Chat { @@ -41,10 +42,6 @@ namespace osu.Game.Online.Chat @"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)", RegexOptions.IgnoreCase); - // 00:00:000 (1,2,3) - test - // regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10 - private static readonly Regex time_regex = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)"); - // #osu private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)"); @@ -274,7 +271,7 @@ namespace osu.Game.Online.Chat handleAdvanced(advanced_link_regex, result, startIndex); // handle editor times - handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); + handleMatches(EditorTimestampParser.TIME_REGEX, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp); // handle channels handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index be1776a330..cde8ee1457 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -563,10 +563,10 @@ namespace osu.Game if (ScreenStack.CurrentScreen is not Editor editor) { Schedule(() => Notifications.Post(new SimpleNotification - { - Icon = FontAwesome.Solid.ExclamationTriangle, + { + Icon = FontAwesome.Solid.ExclamationTriangle, Text = EditorStrings.MustBeInEdit - })); + })); return; } diff --git a/osu.Game/Screens/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs similarity index 50% rename from osu.Game/Screens/Edit/EditorTimestampParser.cs rename to osu.Game/Rulesets/Edit/EditorTimestampParser.cs index 2d8f8a8f4c..4e5a696102 100644 --- a/osu.Game/Screens/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -7,41 +7,43 @@ using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -namespace osu.Game.Screens.Edit +namespace osu.Game.Rulesets.Edit { public static class EditorTimestampParser { - private static readonly Regex timestamp_regex = new Regex(@"^(\d+:\d+:\d+)(?: \((\d+(?:[|,]\d+)*)\))?$", RegexOptions.Compiled); + // 00:00:000 (1,2,3) - test + // regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10 + public static readonly Regex TIME_REGEX = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)"); public static string[] GetRegexGroups(string timestamp) { - Match match = timestamp_regex.Match(timestamp); - return match.Success - ? match.Groups.Values.Where(x => x is not Match).Select(x => x.Value).ToArray() + Match match = TIME_REGEX.Match(timestamp); + string[] result = match.Success + ? match.Groups.Values.Where(x => x is not Match && !x.Value.Contains(':')).Select(x => x.Value).ToArray() : Array.Empty(); + return result; } - public static double GetTotalMilliseconds(string timeGroup) + public static double GetTotalMilliseconds(params string[] timesGroup) { - int[] times = timeGroup.Split(':').Select(int.Parse).ToArray(); + int[] times = timesGroup.Select(int.Parse).ToArray(); Debug.Assert(times.Length == 3); return (times[0] * 60 + times[1]) * 1_000 + times[2]; } - public static List GetSelectedHitObjects(IReadOnlyList editorHitObjects, string objectsGroup, double position) + public static List GetSelectedHitObjects(HitObjectComposer composer, IReadOnlyList editorHitObjects, string objectsGroup, double position) { List hitObjects = editorHitObjects.Where(x => x.StartTime >= position).ToList(); List selectedObjects = new List(); - string[] objectsToSelect = objectsGroup.Split(',').ToArray(); + string[] objectsToSelect = objectsGroup.Split(composer.ObjectSeparator).ToArray(); foreach (string objectInfo in objectsToSelect) { - HitObject? current = hitObjects.FirstOrDefault(x => shouldHitObjectBeSelected(x, objectInfo)); + HitObject? current = hitObjects.FirstOrDefault(x => composer.HandleHitObjectSelection(x, objectInfo)); if (current == null) continue; @@ -67,35 +69,5 @@ namespace osu.Game.Screens.Edit return selectedObjects; } - - private static bool shouldHitObjectBeSelected(HitObject hitObject, string objectInfo) - { - switch (hitObject) - { - // (combo) - case IHasComboInformation comboInfo: - { - if (!double.TryParse(objectInfo, out double comboValue) || comboValue < 1) - return false; - - return comboInfo.IndexInCurrentCombo + 1 == comboValue; - } - - // (time|column) - case IHasColumn column: - { - double[] split = objectInfo.Split('|').Select(double.Parse).ToArray(); - if (split.Length != 2) - return false; - - double timeValue = split[0]; - double columnValue = split[1]; - return hitObject.StartTime == timeValue && column.Column == columnValue; - } - - default: - return false; - } - } } } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 07e5869e28..f6cddcc0d2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -528,6 +528,19 @@ namespace osu.Game.Rulesets.Edit public virtual string ConvertSelectionToString() => string.Empty; + /// + /// The custom logic that decides whether a HitObject should be selected when clicking an editor timestamp link + /// + /// The hitObject being checked + /// A single hitObject's information created with + /// Whether a HitObject should be selected or not + public virtual bool HandleHitObjectSelection(HitObject hitObject, string objectInfo) => false; + + /// + /// A character that separates the selection in + /// + public virtual char ObjectSeparator => ','; + #region IPositionSnapProvider public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9e0671e91d..592e6625cc 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1,4 +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. #nullable disable @@ -14,6 +14,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; @@ -1142,7 +1143,7 @@ namespace osu.Game.Screens.Edit { string[] groups = EditorTimestampParser.GetRegexGroups(timestamp); - if (groups.Length != 2 || string.IsNullOrEmpty(groups[0])) + if (groups.Length != 4 || string.IsNullOrEmpty(groups[0])) { Schedule(() => notifications.Post(new SimpleNotification { @@ -1152,13 +1153,14 @@ namespace osu.Game.Screens.Edit return; } - string timeGroup = groups[0]; - string objectsGroup = groups[1]; - string timeMinutes = timeGroup.Split(':').FirstOrDefault() ?? string.Empty; + string timeMin = groups[0]; + string timeSec = groups[1]; + string timeMss = groups[2]; + string objectsGroup = groups[3].Replace("(", "").Replace(")", "").Trim(); // Currently, lazer chat highlights infinite-long editor links like `10000000000:00:000 (1)` // Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues - if (timeMinutes.Length > 5 || double.Parse(timeMinutes) > 30_000) + if (string.IsNullOrEmpty(timeMin) || timeMin.Length > 5 || double.Parse(timeMin) > 30_000) { Schedule(() => notifications.Post(new SimpleNotification { @@ -1168,38 +1170,36 @@ namespace osu.Game.Screens.Edit return; } - double position = EditorTimestampParser.GetTotalMilliseconds(timeGroup); - editorBeatmap.SelectedHitObjects.Clear(); - // Only seeking is necessary + double position = EditorTimestampParser.GetTotalMilliseconds(timeMin, timeSec, timeMss); + if (string.IsNullOrEmpty(objectsGroup)) { - if (clock.IsRunning) - clock.Stop(); - - clock.Seek(position); + clock.SeekSmoothlyTo(position); return; } + // Seek to the next closest HitObject instead + HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position); + + if (nextObject != null) + position = nextObject.StartTime; + + clock.SeekSmoothlyTo(position); + if (Mode.Value != EditorScreenMode.Compose) Mode.Value = EditorScreenMode.Compose; - // Seek to the next closest HitObject - HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position); - - if (nextObject != null && nextObject.StartTime > 0) - position = nextObject.StartTime; - - List selected = EditorTimestampParser.GetSelectedHitObjects(editorBeatmap.HitObjects.ToList(), objectsGroup, position); + List selected = EditorTimestampParser.GetSelectedHitObjects( + currentScreen.Dependencies.Get(), + editorBeatmap.HitObjects.ToList(), + objectsGroup, + position + ); if (selected.Any()) editorBeatmap.SelectedHitObjects.AddRange(selected); - - if (clock.IsRunning) - clock.Stop(); - - clock.Seek(position); } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From bdbeb2bce4286122584e0c7f9dff61bdc5d57ca1 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Tue, 7 Nov 2023 11:11:32 +0100 Subject: [PATCH 578/896] Renamed CollectionChanged event handler --- .../Edit/Compose/Components/EditorBlueprintContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index b68a690097..a311054ffc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // This makes sure HitObjects will have active Blueprints ready to display // after clicking on an Editor Timestamp/Link - Beatmap.SelectedHitObjects.CollectionChanged += keepHitObjectsAlive; + Beatmap.SelectedHitObjects.CollectionChanged += selectionChanged; if (Composer != null) { @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray()); } - private void keepHitObjectsAlive(object sender, NotifyCollectionChangedEventArgs e) + private void selectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e == null || e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null) return; @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Beatmap.HitObjectAdded -= AddBlueprintFor; Beatmap.HitObjectRemoved -= RemoveBlueprintFor; - Beatmap.SelectedHitObjects.CollectionChanged -= keepHitObjectsAlive; + Beatmap.SelectedHitObjects.CollectionChanged -= selectionChanged; } usageEventBuffer?.Dispose(); From 38c9a98e67e421a965d5979425ace752517e63e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Nov 2023 10:52:48 +0900 Subject: [PATCH 579/896] Add failing test coverage --- .../TestSceneResumeOverlay.cs | 34 +++++++++++++++++-- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 8 ++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs index 49a8254f53..25d0b0a3d3 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneResumeOverlay.cs @@ -1,16 +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 System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; using osu.Game.Tests.Gameplay; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -54,9 +58,13 @@ namespace osu.Game.Rulesets.Osu.Tests [SetUp] public void SetUp() => Schedule(loadContent); - [Test] - public void TestResume() + [TestCase(1)] + [TestCase(0.5f)] + [TestCase(2)] + public void TestResume(float cursorSize) { + AddStep($"set cursor size to {cursorSize}", () => localConfig.SetValue(OsuSetting.GameplayCursorSize, cursorSize)); + AddStep("move mouse to center", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre)); AddStep("show", () => resume.Show()); @@ -64,7 +72,27 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("click", () => osuInputManager.GameClick()); AddAssert("not dismissed", () => !resumeFired && resume.State.Value == Visibility.Visible); - AddStep("move mouse back", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre)); + AddStep("move mouse just out of range", () => + { + var resumeOverlay = this.ChildrenOfType().Single(); + var resumeOverlayCursor = resumeOverlay.ChildrenOfType().Single(); + + Vector2 offset = resumeOverlay.ToScreenSpace(new Vector2(OsuCursor.SIZE / 2)) - resumeOverlay.ToScreenSpace(Vector2.Zero); + InputManager.MoveMouseTo(resumeOverlayCursor.ScreenSpaceDrawQuad.Centre - offset - new Vector2(1)); + }); + + AddStep("click", () => osuInputManager.GameClick()); + AddAssert("not dismissed", () => !resumeFired && resume.State.Value == Visibility.Visible); + + AddStep("move mouse just within range", () => + { + var resumeOverlay = this.ChildrenOfType().Single(); + var resumeOverlayCursor = resumeOverlay.ChildrenOfType().Single(); + + Vector2 offset = resumeOverlay.ToScreenSpace(new Vector2(OsuCursor.SIZE / 2)) - resumeOverlay.ToScreenSpace(Vector2.Zero); + InputManager.MoveMouseTo(resumeOverlayCursor.ScreenSpaceDrawQuad.Centre - offset + new Vector2(1)); + }); + AddStep("click", () => osuInputManager.GameClick()); AddAssert("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden); } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index ab1bb0cf5a..8215201d43 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { public partial class OsuCursor : SkinReloadableDrawable { - private const float size = 28; + public const float SIZE = 28; private const float pressed_scale = 1.2f; private const float released_scale = 1f; @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { Origin = Anchor.Centre; - Size = new Vector2(size); + Size = new Vector2(SIZE); } [BackgroundDependencyLoader] @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Masking = true, - BorderThickness = size / 6, + BorderThickness = SIZE / 6, BorderColour = Color4.White, EdgeEffect = new EdgeEffectParameters { @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Anchor = Anchor.Centre, RelativeSizeAxes = Axes.Both, Masking = true, - BorderThickness = size / 3, + BorderThickness = SIZE / 3, BorderColour = Color4.White.Opacity(0.5f), Children = new Drawable[] { From 544d5d1d86416a179de9c6badd7833a8e53ca518 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Tue, 7 Nov 2023 12:23:22 +0100 Subject: [PATCH 580/896] Forgot a clock.Stop call --- osu.Game/Screens/Edit/Editor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 592e6625cc..58c3ae809c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1145,7 +1145,7 @@ namespace osu.Game.Screens.Edit if (groups.Length != 4 || string.IsNullOrEmpty(groups[0])) { - Schedule(() => notifications.Post(new SimpleNotification + Schedule(() => notifications?.Post(new SimpleNotification { Icon = FontAwesome.Solid.ExclamationTriangle, Text = EditorStrings.FailedToProcessTimestamp @@ -1162,7 +1162,7 @@ namespace osu.Game.Screens.Edit // Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues if (string.IsNullOrEmpty(timeMin) || timeMin.Length > 5 || double.Parse(timeMin) > 30_000) { - Schedule(() => notifications.Post(new SimpleNotification + Schedule(() => notifications?.Post(new SimpleNotification { Icon = FontAwesome.Solid.ExclamationTriangle, Text = EditorStrings.TooLongTimestamp @@ -1172,6 +1172,9 @@ namespace osu.Game.Screens.Edit editorBeatmap.SelectedHitObjects.Clear(); + if (clock.IsRunning) + clock.Stop(); + double position = EditorTimestampParser.GetTotalMilliseconds(timeMin, timeSec, timeMss); if (string.IsNullOrEmpty(objectsGroup)) From 81caa854e6a7ba3251658f1cd174bf6bad62b1ea Mon Sep 17 00:00:00 2001 From: ratinfx Date: Tue, 7 Nov 2023 13:02:46 +0100 Subject: [PATCH 581/896] Separate Test cases by relevant rulesets --- .../TestSceneOpenEditorTimestampInMania.cs | 102 +++++++ .../TestSceneOpenEditorTimestampInOsu.cs | 110 ++++++++ .../Editing/TestSceneOpenEditorTimestamp.cs | 249 ++++-------------- 3 files changed, 259 insertions(+), 202 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs new file mode 100644 index 0000000000..6ec5dcee4c --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Editor +{ + public partial class TestSceneOpenEditorTimestampInMania : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); + + private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true) + { + AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp)); + AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); + } + + private void addReset() + { + addStepClickLink("00:00:000", "reset", false); + } + + private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null) + { + bool checkColumns = columnPairs != null + ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime + && EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0) + && checkColumns; + } + + private bool isNoteAt(HitObject hitObject, double time, int column) + { + return hitObject is ManiaHitObject maniaHitObject + && maniaHitObject.StartTime == time + && maniaHitObject.Column == column; + } + + [Test] + public void TestNormalSelection() + { + addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)"); + AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)> + { (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) } + )); + + addReset(); + addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)"); + AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)> + { (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) } + )); + + addReset(); + AddStep("add notes to row", () => + { + if (EditorBeatmap.HitObjects.Any(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column is 1 or 2 or 3)) + return; + + ManiaHitObject first = (ManiaHitObject)EditorBeatmap.HitObjects.First(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column == 0); + ManiaHitObject second = new Note { Column = 1, StartTime = first.StartTime }; + ManiaHitObject third = new Note { Column = 2, StartTime = first.StartTime }; + ManiaHitObject forth = new Note { Column = 3, StartTime = first.StartTime }; + EditorBeatmap.AddRange(new[] { second, third, forth }); + }); + addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)"); + AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)> + { (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) } + )); + + addReset(); + addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)"); + AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)> + { (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) } + )); + } + + [Test] + public void TestUnusualSelection() + { + addStepClickLink("00:00:000 (0|1)", "invalid link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); + + addReset(); + addStepClickLink("00:00:000 (0)", "std link"); + AddAssert("snap and select 1", () => checkSnapAndSelectColumn(2_170, new List<(int, int)> + { (2_170, 2) }) + ); + + addReset(); + // TODO: discuss - this selects the first 2 objects on Stable, do we want that or is this fine? + addStepClickLink("00:00:000 (1,2)", "std link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs new file mode 100644 index 0000000000..d69f482d29 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs @@ -0,0 +1,110 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public partial class TestSceneOpenEditorTimestampInOsu : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true) + { + AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp)); + AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); + } + + private void addReset() + { + addStepClickLink("00:00:000", "reset", false); + } + + private bool checkSnapAndSelectCombo(double startTime, params int[] comboNumbers) + { + bool checkCombos = comboNumbers.Any() + ? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime + && EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length + && checkCombos; + } + + private bool hasCombosInOrder(IEnumerable selected, params int[] comboNumbers) + { + List hitObjects = selected.ToList(); + if (hitObjects.Count != comboNumbers.Length) + return false; + + return !hitObjects.Select(x => (OsuHitObject)x) + .Where((x, i) => x.IndexInCurrentCombo + 1 != comboNumbers[i]) + .Any(); + } + + [Test] + public void TestNormalSelection() + { + addStepClickLink("00:02:170 (1,2,3)"); + AddAssert("snap and select 1-2-3", () => checkSnapAndSelectCombo(2_170, 1, 2, 3)); + + addReset(); + addStepClickLink("00:04:748 (2,3,4,1,2)"); + AddAssert("snap and select 2-3-4-1-2", () => checkSnapAndSelectCombo(4_748, 2, 3, 4, 1, 2)); + + addReset(); + addStepClickLink("00:02:170 (1,1,1)"); + AddAssert("snap and select 1-1-1", () => checkSnapAndSelectCombo(2_170, 1, 1, 1)); + + addReset(); + addStepClickLink("00:02:873 (2,2,2,2)"); + AddAssert("snap and select 2-2-2-2", () => checkSnapAndSelectCombo(2_873, 2, 2, 2, 2)); + } + + [Test] + public void TestUnusualSelection() + { + HitObject firstObject = null!; + + addStepClickLink("00:00:000 (1,2,3)", "invalid offset"); + AddAssert("snap to next, select 1-2-3", () => + { + firstObject = EditorBeatmap.HitObjects.First(); + return checkSnapAndSelectCombo(firstObject.StartTime, 1, 2, 3); + }); + + addReset(); + addStepClickLink("00:00:956 (2,3,4)", "invalid offset"); + AddAssert("snap to next, select 2-3-4", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3, 4)); + + addReset(); + addStepClickLink("00:00:000 (0)", "invalid offset"); + AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); + + addReset(); + addStepClickLink("00:00:000 (1)", "invalid offset"); + AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); + + addReset(); + addStepClickLink("00:00:000 (2)", "invalid offset"); + AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); + + addReset(); + addStepClickLink("00:00:000 (2,3)", "invalid offset"); + AddAssert("snap to 1, select 2-3", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3)); + + addReset(); + addStepClickLink("00:00:956 (956|1,956|2)", "mania link"); + AddAssert("snap to next, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); + + addReset(); + addStepClickLink("00:00:000 (0|1)", "mania link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); + } + } +} diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index f7b976702a..bc31924e2c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.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 System.Linq; using NUnit.Framework; using osu.Framework.Extensions; @@ -12,9 +11,6 @@ using osu.Game.Database; using osu.Game.Localisation; using osu.Game.Online.Chat; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mania; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; @@ -25,79 +21,39 @@ namespace osu.Game.Tests.Visual.Editing { public partial class TestSceneOpenEditorTimestamp : OsuGameTestScene { - protected Editor Editor => (Editor)Game.ScreenStack.CurrentScreen; - protected EditorBeatmap EditorBeatmap => Editor.ChildrenOfType().Single(); - protected EditorClock EditorClock => Editor.ChildrenOfType().Single(); + private Editor editor => (Editor)Game.ScreenStack.CurrentScreen; + private EditorBeatmap editorBeatmap => editor.ChildrenOfType().Single(); + private EditorClock editorClock => editor.ChildrenOfType().Single(); - protected void AddStepClickLink(string timestamp, string step = "", bool waitForSeek = true) + private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true) { AddStep($"{step} {timestamp}", () => Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp)) ); if (waitForSeek) - AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); + AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value); } - protected void AddStepScreenModeTo(EditorScreenMode screenMode) + private void addStepScreenModeTo(EditorScreenMode screenMode) { - AddStep("change screen to " + screenMode, () => Editor.Mode.Value = screenMode); + AddStep("change screen to " + screenMode, () => editor.Mode.Value = screenMode); } - protected void AssertOnScreenAt(EditorScreenMode screen, double time, string text = "stayed in") + private void assertOnScreenAt(EditorScreenMode screen, double time, string text = "stayed in") { AddAssert($"{text} {screen} at {time}", () => - Editor.Mode.Value == screen - && EditorClock.CurrentTime == time + editor.Mode.Value == screen + && editorClock.CurrentTime == time ); } - protected void AssertMovedScreenTo(EditorScreenMode screen, string text = "moved to") + private void assertMovedScreenTo(EditorScreenMode screen, string text = "moved to") { - AddAssert($"{text} {screen}", () => Editor.Mode.Value == screen); + AddAssert($"{text} {screen}", () => editor.Mode.Value == screen); } - private bool checkSnapAndSelectCombo(double startTime, params int[] comboNumbers) - { - bool checkCombos = comboNumbers.Any() - ? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers) - : !EditorBeatmap.SelectedHitObjects.Any(); - - return EditorClock.CurrentTime == startTime - && EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length - && checkCombos; - } - - private bool hasCombosInOrder(IEnumerable selected, params int[] comboNumbers) - { - List hitObjects = selected.ToList(); - if (hitObjects.Count != comboNumbers.Length) - return false; - - return !hitObjects.Select(x => (IHasComboInformation)x) - .Where((combo, i) => combo.IndexInCurrentCombo + 1 != comboNumbers[i]) - .Any(); - } - - private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null) - { - bool checkColumns = columnPairs != null - ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) - : !EditorBeatmap.SelectedHitObjects.Any(); - - return EditorClock.CurrentTime == startTime - && EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0) - && checkColumns; - } - - private bool isNoteAt(HitObject hitObject, double time, int column) - { - return hitObject is IHasColumn columnInfo - && hitObject.StartTime == time - && columnInfo.Column == column; - } - - protected void SetUpEditor(RulesetInfo ruleset) + private void setUpEditor(RulesetInfo ruleset) { BeatmapSetInfo beatmapSet = null!; @@ -118,7 +74,7 @@ namespace osu.Game.Tests.Visual.Editing ((PlaySongSelect)Game.ScreenStack.CurrentScreen) .Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name)) ); - AddUntilStep("Wait for editor open", () => Editor.ReadyForUse); + AddUntilStep("Wait for editor open", () => editor.ReadyForUse); } [Test] @@ -126,7 +82,7 @@ namespace osu.Game.Tests.Visual.Editing { RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; - AddStepClickLink("00:00:000", waitForSeek: false); + addStepClickLink("00:00:000", waitForSeek: false); AddAssert("recieved 'must be in edit'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 1 ); @@ -134,20 +90,20 @@ namespace osu.Game.Tests.Visual.Editing AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); - AddStepClickLink("00:00:000 (1)", waitForSeek: false); + addStepClickLink("00:00:000 (1)", waitForSeek: false); AddAssert("recieved 'must be in edit'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 2 ); - SetUpEditor(rulesetInfo); - AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + setUpEditor(rulesetInfo); + AddAssert("is editor Osu", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - AddStepClickLink("00:000", "invalid link", waitForSeek: false); + addStepClickLink("00:000", "invalid link", waitForSeek: false); AddAssert("recieved 'failed to process'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 1 ); - AddStepClickLink("50000:00:000", "too long link", waitForSeek: false); + addStepClickLink("50000:00:000", "too long link", waitForSeek: false); AddAssert("recieved 'too long'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.TooLongTimestamp) == 1 ); @@ -159,156 +115,45 @@ namespace osu.Game.Tests.Visual.Editing const long long_link_value = 1_000 * 60 * 1_000; RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; - SetUpEditor(rulesetInfo); - AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + setUpEditor(rulesetInfo); + AddAssert("is editor Osu", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - AddStepClickLink("1000:00:000", "long link"); + addStepClickLink("1000:00:000", "long link"); AddAssert("moved to end of track", () => - EditorClock.CurrentTime == long_link_value - || (EditorClock.TrackLength < long_link_value && EditorClock.CurrentTime == EditorClock.TrackLength) + editorClock.CurrentTime == long_link_value + || (editorClock.TrackLength < long_link_value && editorClock.CurrentTime == editorClock.TrackLength) ); - AddStepScreenModeTo(EditorScreenMode.SongSetup); - AddStepClickLink("00:00:000"); - AssertOnScreenAt(EditorScreenMode.SongSetup, 0); + addStepScreenModeTo(EditorScreenMode.SongSetup); + addStepClickLink("00:00:000"); + assertOnScreenAt(EditorScreenMode.SongSetup, 0); - AddStepClickLink("00:05:000 (0|0)"); - AssertMovedScreenTo(EditorScreenMode.Compose); + addStepClickLink("00:05:000 (0|0)"); + assertMovedScreenTo(EditorScreenMode.Compose); - AddStepScreenModeTo(EditorScreenMode.Design); - AddStepClickLink("00:10:000"); - AssertOnScreenAt(EditorScreenMode.Design, 10_000); + addStepScreenModeTo(EditorScreenMode.Design); + addStepClickLink("00:10:000"); + assertOnScreenAt(EditorScreenMode.Design, 10_000); - AddStepClickLink("00:15:000 (1)"); - AssertMovedScreenTo(EditorScreenMode.Compose); + addStepClickLink("00:15:000 (1)"); + assertMovedScreenTo(EditorScreenMode.Compose); - AddStepScreenModeTo(EditorScreenMode.Timing); - AddStepClickLink("00:20:000"); - AssertOnScreenAt(EditorScreenMode.Timing, 20_000); + addStepScreenModeTo(EditorScreenMode.Timing); + addStepClickLink("00:20:000"); + assertOnScreenAt(EditorScreenMode.Timing, 20_000); - AddStepClickLink("00:25:000 (0,1)"); - AssertMovedScreenTo(EditorScreenMode.Compose); + addStepClickLink("00:25:000 (0,1)"); + assertMovedScreenTo(EditorScreenMode.Compose); - AddStepScreenModeTo(EditorScreenMode.Verify); - AddStepClickLink("00:30:000"); - AssertOnScreenAt(EditorScreenMode.Verify, 30_000); + addStepScreenModeTo(EditorScreenMode.Verify); + addStepClickLink("00:30:000"); + assertOnScreenAt(EditorScreenMode.Verify, 30_000); - AddStepClickLink("00:35:000 (0,1)"); - AssertMovedScreenTo(EditorScreenMode.Compose); + addStepClickLink("00:35:000 (0,1)"); + assertMovedScreenTo(EditorScreenMode.Compose); - AddStepClickLink("00:00:000"); - AssertOnScreenAt(EditorScreenMode.Compose, 0); - } - - [Test] - public void TestSelectionForOsu() - { - HitObject firstObject = null!; - RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; - - SetUpEditor(rulesetInfo); - AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - - AddStepClickLink("00:00:956 (1,2,3)"); - AddAssert("snap and select 1-2-3", () => - { - firstObject = EditorBeatmap.HitObjects.First(); - return checkSnapAndSelectCombo(firstObject.StartTime, 1, 2, 3); - }); - - AddStepClickLink("00:01:450 (2,3,4,1,2)"); - AddAssert("snap and select 2-3-4-1-2", () => checkSnapAndSelectCombo(1_450, 2, 3, 4, 1, 2)); - - AddStepClickLink("00:00:956 (1,1,1)"); - AddAssert("snap and select 1-1-1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1, 1, 1)); - } - - [Test] - public void TestUnusualSelectionForOsu() - { - HitObject firstObject = null!; - RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; - - SetUpEditor(rulesetInfo); - AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - - AddStepClickLink("00:00:000 (1,2,3)", "invalid offset"); - AddAssert("snap to next, select 1-2-3", () => - { - firstObject = EditorBeatmap.HitObjects.First(); - return checkSnapAndSelectCombo(firstObject.StartTime, 1, 2, 3); - }); - - AddStepClickLink("00:00:956 (2,3,4)", "invalid offset"); - AddAssert("snap to next, select 2-3-4", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3, 4)); - - AddStepClickLink("00:00:000 (0)", "invalid offset"); - AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - - AddStepClickLink("00:00:000 (1)", "invalid offset"); - AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - - AddStepClickLink("00:00:000 (2)", "invalid offset"); - AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - - AddStepClickLink("00:00:000 (2,3)", "invalid offset"); - AddAssert("snap to 1, select 2-3", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3)); - - AddStepClickLink("00:00:956 (956|1,956|2)", "mania link"); - AddAssert("snap to next, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); - - AddStepClickLink("00:00:000 (0|1)", "mania link"); - AddAssert("snap to 1, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); - } - - [Test] - public void TestSelectionForMania() - { - RulesetInfo rulesetInfo = new ManiaRuleset().RulesetInfo; - - SetUpEditor(rulesetInfo); - AddAssert("is editor Mania", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - - AddStepClickLink("00:11:010 (11010|1,11175|5,11258|3,11340|5,11505|1)"); - AddAssert("selected group", () => checkSnapAndSelectColumn(11010, new List<(int, int)> - { (11010, 1), (11175, 5), (11258, 3), (11340, 5), (11505, 1) } - )); - - AddStepClickLink("00:00:956 (956|1,956|6,1285|3,1780|4)"); - AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(956, new List<(int, int)> - { (956, 1), (956, 6), (1285, 3), (1780, 4) } - )); - - AddStepClickLink("02:36:560 (156560|1,156560|4,156560|6)"); - AddAssert("selected in row", () => checkSnapAndSelectColumn(156560, new List<(int, int)> - { (156560, 1), (156560, 4), (156560, 6) } - )); - - AddStepClickLink("00:35:736 (35736|3,36395|3,36725|3,37384|3)"); - AddAssert("selected in column", () => checkSnapAndSelectColumn(35736, new List<(int, int)> - { (35736, 3), (36395, 3), (36725, 3), (37384, 3) } - )); - } - - [Test] - public void TestUnusualSelectionForMania() - { - RulesetInfo rulesetInfo = new ManiaRuleset().RulesetInfo; - - SetUpEditor(rulesetInfo); - AddAssert("is editor Mania", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - - AddStepClickLink("00:00:000 (0|1)", "invalid link"); - AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(956)); - - AddStepClickLink("00:00:000 (0)", "std link"); - AddAssert("snap and select 1", () => checkSnapAndSelectColumn(956, new List<(int, int)> - { (956, 1) }) - ); - - // TODO: discuss - this selects the first 2 objects on Stable, do we want that or is this fine? - AddStepClickLink("00:00:000 (1,2)", "std link"); - AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(956)); + addStepClickLink("00:00:000"); + assertOnScreenAt(EditorScreenMode.Compose, 0); } } } From fcd73e62d2f98472e66524913c5f58fa2499d4f2 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 7 Nov 2023 13:06:14 +0100 Subject: [PATCH 582/896] Remove mobile specific changes Will be added back in a separate PR --- osu.Android/OsuGameAndroid.cs | 3 --- .../Overlays/Settings/Sections/Input/TouchSettings.cs | 11 ++++------- osu.Game/Screens/Play/PlayerSettings/InputSettings.cs | 5 ++--- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index e4b934a387..97a9848a12 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -98,9 +98,6 @@ namespace osu.Android case AndroidJoystickHandler jh: return new AndroidJoystickSettings(jh); - case AndroidTouchHandler: - return new TouchSettings(handler); - default: return base.CreateSettingsSubsectionFor(handler); } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs index 0056de6674..30a0b1b785 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs @@ -29,14 +29,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) { - if (!RuntimeInfo.IsMobile) // don't allow disabling the only input method (touch) on mobile. + Add(new SettingsCheckbox { - Add(new SettingsCheckbox - { - LabelText = CommonStrings.Enabled, - Current = handler.Enabled - }); - } + LabelText = CommonStrings.Enabled, + Current = handler.Enabled + }); Add(new SettingsCheckbox { diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 1387e01305..47af4e0b53 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -23,9 +23,8 @@ namespace osu.Game.Screens.Play.PlayerSettings { new PlayerCheckbox { - // TODO: change to touchscreen detection once https://github.com/ppy/osu/pull/25348 makes it in - LabelText = RuntimeInfo.IsDesktop ? MouseSettingsStrings.DisableClicksDuringGameplay : TouchSettingsStrings.DisableTapsDuringGameplay, - Current = config.GetBindable(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.TouchDisableGameplayTaps) + LabelText = MouseSettingsStrings.DisableClicksDuringGameplay, + Current = config.GetBindable(OsuSetting.MouseDisableButtons) } }; } From 7bedf7cf165e1a9a125eb74af636dca957e8fc0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Nov 2023 21:08:49 +0900 Subject: [PATCH 583/896] Move static method to end of file --- osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs index 8215201d43..ba9fda25e4 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs @@ -84,12 +84,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor calculateCursorScale(); } - /// - /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size. - /// - public static float GetScaleForCircleSize(float circleSize) => - 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY; - private void calculateCursorScale() { float scale = userCursorScale.Value; @@ -117,6 +111,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public void Contract() => expandTarget.ScaleTo(released_scale, 400, Easing.OutQuad); + /// + /// Get the scale applicable to the ActiveCursor based on a beatmap's circle size. + /// + public static float GetScaleForCircleSize(float circleSize) => + 1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY; + private partial class DefaultCursor : OsuCursorSprite { public DefaultCursor() From 00268d0ccce3691e682f7ae2ff08cb6a9a1f88f8 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 7 Nov 2023 13:09:30 +0100 Subject: [PATCH 584/896] Remove unused using --- osu.Android/OsuGameAndroid.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 97a9848a12..dea70e6b27 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -11,7 +11,6 @@ using osu.Framework.Input.Handlers; using osu.Framework.Platform; using osu.Game; using osu.Game.Overlays.Settings; -using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Updater; using osu.Game.Utils; From 02d9f005d09df3bd755a337a7d63c3ef4353d8b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Nov 2023 21:27:02 +0900 Subject: [PATCH 585/896] Update squirrel to latest release Quite a few fixes since our last update. See https://github.com/clowd/Clowd.Squirrel/releases. Of special note is [a fix for incomplete updates](https://github.com/clowd/Clowd.Squirrel/issues/182) which I believe we've seen cause issues in the wild before. --- 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 1d43e118a3..f37cfdc5f1 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -23,7 +23,7 @@ - + From 3e257f1e6c02795c6bbb6bf08f542707e8e6e827 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Nov 2023 23:21:51 +0900 Subject: [PATCH 586/896] Remove unused using statements --- osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs | 1 - osu.Game/Screens/Play/PlayerSettings/InputSettings.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs index 30a0b1b785..175fcc4709 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Input.Handlers; using osu.Framework.Localisation; diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 47af4e0b53..852fbd8dcc 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.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 osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; From b092b0093affc97453d4f14be7b7a68005d5ae31 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 6 Nov 2023 21:13:36 +0300 Subject: [PATCH 587/896] Make sure bar draw quad is thick enough --- osu.Game/Graphics/UserInterface/BarGraph.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index 27a41eb7e3..d3eebd71f0 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -145,6 +145,13 @@ namespace osu.Game.Graphics.UserInterface float barHeight = drawSize.Y * ((direction == BarDirection.TopToBottom || direction == BarDirection.BottomToTop) ? lengths[i] : barBreadth); float barWidth = drawSize.X * ((direction == BarDirection.LeftToRight || direction == BarDirection.RightToLeft) ? lengths[i] : barBreadth); + if (barHeight == 0 || barWidth == 0) + continue; + + // Make sure draw quad is thick enough + barHeight = Math.Max(barHeight, 1.5f); + barWidth = Math.Max(barWidth, 1.5f); + Vector2 topLeft; switch (direction) From cbea2db4bef9322b8dcbe9619d95cb918dcc3b55 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 7 Nov 2023 02:03:16 +0300 Subject: [PATCH 588/896] Support absolute-sized health bar and use it for default layout --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 30 +++++++++++++------ osu.Game/Skinning/ArgonSkin.cs | 3 +- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 793d43f7ef..2ae6bdcb15 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -36,12 +36,10 @@ namespace osu.Game.Screens.Play.HUD }; [SettingSource("Bar length")] - public BindableFloat BarLength { get; } = new BindableFloat(0.98f) - { - MinValue = 0.2f, - MaxValue = 1, - Precision = 0.01f, - }; + public BindableFloat BarLength { get; } = new BindableFloat(0.98f); + + [SettingSource("Use relative size")] + public BindableBool UseRelativeSize { get; } = new BindableBool(true); private BarPath mainBar = null!; @@ -140,9 +138,23 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(_ => Scheduler.AddOnce(updateCurrent), true); - BarLength.BindValueChanged(l => Width = l.NewValue, true); - BarHeight.BindValueChanged(_ => updatePath()); - updatePath(); + // update relative axes first before reading width from bar length. + RelativeSizeAxes = UseRelativeSize.Value ? Axes.X : Axes.None; + Width = BarLength.Value; + + UseRelativeSize.BindValueChanged(v => + { + RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None; + float newWidth = Width; + + BarLength.MinValue = v.NewValue ? 0.2f : 200f; + BarLength.MaxValue = v.NewValue ? 1f : 1000f; + BarLength.Precision = v.NewValue ? 0.01f : 1f; + BarLength.Value = newWidth; + }, true); + + BarLength.ValueChanged += l => Width = l.NewValue; + BarHeight.BindValueChanged(_ => updatePath(), true); } protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 82c150ced7..95e1820059 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -129,7 +129,8 @@ namespace osu.Game.Skinning health.Anchor = Anchor.TopLeft; health.Origin = Anchor.TopLeft; - health.BarLength.Value = 0.22f; + health.UseRelativeSize.Value = false; + health.BarLength.Value = 300; health.BarHeight.Value = 30f; health.Position = new Vector2(components_x_offset, 20f); From e6d3085353886da1a196f6d0354eae86ba9469fa Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Nov 2023 01:48:21 +0300 Subject: [PATCH 589/896] Update accuracy counter design --- .../Screens/Play/HUD/ArgonAccuracyCounter.cs | 20 +++++++++++++++---- .../Play/HUD/ArgonCounterTextComponent.cs | 8 ++++---- osu.Game/Skinning/ArgonSkin.cs | 14 ++++++------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index 33223a526b..0414cbaea4 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -1,18 +1,30 @@ // 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.Game.Graphics; -using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Screens.Play.HUD { public partial class ArgonAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable { + [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) + { + Precision = 0.01f, + MinValue = 0, + MaxValue = 1, + }; + public bool UsesFixedAnchor { get; set; } - protected override OsuSpriteText CreateSpriteText() - => base.CreateSpriteText().With(s => s.Font = OsuFont.Default.With(size: 19.2f)); + protected override IHasText CreateText() => new ArgonCounterTextComponent(Anchor.TopLeft, "ACCURACY", new Vector2(-4, 0)) + { + WireframeOpacity = { BindTarget = WireframeOpacity }, + }; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 9545168a46..fcad0f12d8 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play.HUD } } - public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null) + public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null, Vector2? spacing = null) { Anchor = anchor; Origin = anchor; @@ -49,11 +49,13 @@ namespace osu.Game.Screens.Play.HUD { Anchor = anchor, Origin = anchor, + Spacing = spacing ?? new Vector2(-2, 0), }; textPart = new ArgonCounterSpriteText { Anchor = anchor, Origin = anchor, + Spacing = spacing ?? new Vector2(-2, 0), }; } @@ -115,9 +117,7 @@ namespace osu.Game.Screens.Play.HUD private void load(ISkinSource skin) { // todo: rename font - Font = new FontUsage(@"argon-score", 1, fixedWidth: true); - Spacing = new Vector2(-2, 0); - + Font = new FontUsage(@"argon-score", 1); glyphStore = new GlyphStore(skin, glyphLookupOverride); } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 95e1820059..2af3a29804 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -143,14 +143,14 @@ namespace osu.Game.Skinning score.Origin = Anchor.TopRight; score.Position = new Vector2(components_x_offset + 200, scoreWedge.Y + 30); } + } - if (accuracy != null) - { - // +4 to vertically align the accuracy counter with the score counter. - accuracy.Position = new Vector2(components_x_offset + 4, scoreWedge.Y + 45); - accuracy.Anchor = Anchor.TopLeft; - accuracy.Origin = Anchor.TopLeft; - } + if (accuracy != null) + { + // +4 to vertically align the accuracy counter with the score counter. + accuracy.Position = new Vector2(-20, 20); + accuracy.Anchor = Anchor.TopRight; + accuracy.Origin = Anchor.TopRight; } var hitError = container.OfType().FirstOrDefault(); From d30bac3f49af02f2473e9c1a71aacbdb8f42dd4e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Nov 2023 01:49:13 +0300 Subject: [PATCH 590/896] Move "required display digits" feature to reside in argon score counter --- .../Screens/Play/HUD/ArgonCounterTextComponent.cs | 7 +++---- osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index fcad0f12d8..437a627cbc 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.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. -using System; -using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -25,7 +23,6 @@ namespace osu.Game.Screens.Play.HUD private readonly ArgonCounterSpriteText wireframesPart; private readonly ArgonCounterSpriteText textPart; - public IBindable RequiredDisplayDigits { get; } = new BindableInt(); public IBindable WireframeOpacity { get; } = new BindableFloat(); public LocalisableString Text @@ -33,7 +30,7 @@ namespace osu.Game.Screens.Play.HUD get => textPart.Text; set { - wireframesPart.Text = new string('#', Math.Max(value.ToString().Count(char.IsDigit), RequiredDisplayDigits.Value)); + wireframesPart.Text = FormatWireframes(value); textPart.Text = value; } } @@ -91,6 +88,8 @@ namespace osu.Game.Screens.Play.HUD }; } + protected virtual LocalisableString FormatWireframes(LocalisableString text) => text; + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 636565f181..fef4199d31 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.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.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -25,10 +26,22 @@ namespace osu.Game.Screens.Play.HUD protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(); - protected override IHasText CreateText() => new ArgonCounterTextComponent(Anchor.TopRight) + protected override IHasText CreateText() => new ArgonScoreTextComponent(Anchor.TopRight) { RequiredDisplayDigits = { BindTarget = RequiredDisplayDigits }, WireframeOpacity = { BindTarget = WireframeOpacity }, }; + + private partial class ArgonScoreTextComponent : ArgonCounterTextComponent + { + public IBindable RequiredDisplayDigits { get; } = new BindableInt(); + + public ArgonScoreTextComponent(Anchor anchor, LocalisableString? label = null) + : base(anchor, label) + { + } + + protected override LocalisableString FormatWireframes(LocalisableString text) => new string('#', Math.Max(text.ToString().Length, RequiredDisplayDigits.Value)); + } } } From fdc714a248d1a9283649d12a11a14e7763bfa1dc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Nov 2023 02:05:19 +0300 Subject: [PATCH 591/896] Support percentages and ignore dot characters in wireframes part --- .../Play/HUD/ArgonCounterTextComponent.cs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 437a627cbc..3d8546e0e3 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.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.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -42,13 +43,28 @@ namespace osu.Game.Screens.Play.HUD this.label = label; - wireframesPart = new ArgonCounterSpriteText(@"wireframes") + wireframesPart = new ArgonCounterSpriteText(c => + { + if (c == '.') + return @"dot"; + + return @"wireframes"; + }) { Anchor = anchor, Origin = anchor, Spacing = spacing ?? new Vector2(-2, 0), }; - textPart = new ArgonCounterSpriteText + textPart = new ArgonCounterSpriteText(c => + { + if (c == '.') + return @"dot"; + + if (c == '%') + return @"percentage"; + + return c.ToString(); + }) { Anchor = anchor, Origin = anchor, @@ -98,15 +114,15 @@ namespace osu.Game.Screens.Play.HUD private partial class ArgonCounterSpriteText : OsuSpriteText { - private readonly string? glyphLookupOverride; + private readonly Func getLookup; private GlyphStore glyphStore = null!; protected override char FixedWidthReferenceCharacter => '5'; - public ArgonCounterSpriteText(string? glyphLookupOverride = null) + public ArgonCounterSpriteText(Func getLookup) { - this.glyphLookupOverride = glyphLookupOverride; + this.getLookup = getLookup; Shadow = false; UseFullGlyphHeight = false; @@ -117,7 +133,7 @@ namespace osu.Game.Screens.Play.HUD { // todo: rename font Font = new FontUsage(@"argon-score", 1); - glyphStore = new GlyphStore(skin, glyphLookupOverride); + glyphStore = new GlyphStore(skin, getLookup); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); @@ -125,17 +141,17 @@ namespace osu.Game.Screens.Play.HUD private class GlyphStore : ITexturedGlyphLookupStore { private readonly ISkin skin; - private readonly string? glyphLookupOverride; + private readonly Func getLookup; - public GlyphStore(ISkin skin, string? glyphLookupOverride) + public GlyphStore(ISkin skin, Func getLookup) { this.skin = skin; - this.glyphLookupOverride = glyphLookupOverride; + this.getLookup = getLookup; } public ITexturedCharacterGlyph? Get(string fontName, char character) { - string lookup = glyphLookupOverride ?? character.ToString(); + string lookup = getLookup(character); var texture = skin.GetTexture($"{fontName}-{lookup}"); if (texture == null) From 4de5454538b0dff1b8b4a020a7f12fcb294ccc06 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Nov 2023 02:06:51 +0300 Subject: [PATCH 592/896] Bring back left-side line next to health display Makes the score counter not look weird when it reaches 8 digits --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 9 +++--- .../Screens/Play/HUD/ArgonHealthRightLine.cs | 30 +++++++++++++++++++ osu.Game/Skinning/ArgonSkin.cs | 4 +++ 3 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/ArgonHealthRightLine.cs diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 2ae6bdcb15..b2b3181d08 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -90,12 +90,11 @@ namespace osu.Game.Screens.Play.HUD } } - private const float main_path_radius = 10f; + public const float MAIN_PATH_RADIUS = 10f; [BackgroundDependencyLoader] private void load() { - RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; InternalChild = new Container @@ -105,7 +104,7 @@ namespace osu.Game.Screens.Play.HUD { background = new BackgroundPath { - PathRadius = main_path_radius, + PathRadius = MAIN_PATH_RADIUS, }, glowBar = new BarPath { @@ -125,7 +124,7 @@ namespace osu.Game.Screens.Play.HUD Blending = BlendingParameters.Additive, BarColour = main_bar_colour, GlowColour = main_bar_glow_colour, - PathRadius = main_path_radius, + PathRadius = MAIN_PATH_RADIUS, GlowPortion = 0.6f, }, } @@ -248,7 +247,7 @@ namespace osu.Game.Screens.Play.HUD private void updatePath() { - float barLength = DrawWidth - main_path_radius * 2; + float barLength = DrawWidth - MAIN_PATH_RADIUS * 2; float curveStart = barLength - 70; float curveEnd = barLength - 40; diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthRightLine.cs b/osu.Game/Screens/Play/HUD/ArgonHealthRightLine.cs new file mode 100644 index 0000000000..25918f679c --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ArgonHealthRightLine.cs @@ -0,0 +1,30 @@ +// 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.Game.Skinning; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public partial class ArgonHealthRightLine : CompositeDrawable, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(50f, ArgonHealthDisplay.MAIN_PATH_RADIUS * 2); + InternalChild = new Circle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 45f, + Height = 3f, + }; + } + } +} diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 2af3a29804..e9953b57a7 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -115,6 +115,7 @@ namespace osu.Game.Skinning var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { var health = container.OfType().FirstOrDefault(); + var healthLine = container.OfType().FirstOrDefault(); var scoreWedge = container.OfType().FirstOrDefault(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); @@ -134,6 +135,9 @@ namespace osu.Game.Skinning health.BarHeight.Value = 30f; health.Position = new Vector2(components_x_offset, 20f); + if (healthLine != null) + healthLine.Y = health.Y; + if (scoreWedge != null) { scoreWedge.Position = new Vector2(-50, 15); From 07b7e13633862e85522f0c1778c8dc06ee62724d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Nov 2023 02:07:24 +0300 Subject: [PATCH 593/896] Place health display in front of the score wedge --- osu.Game/Skinning/ArgonSkin.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index e9953b57a7..d598c04891 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -204,9 +204,10 @@ namespace osu.Game.Skinning { Children = new Drawable[] { - new ArgonHealthDisplay(), new ArgonScoreWedge(), new ArgonScoreCounter(), + new ArgonHealthDisplay(), + new ArgonHealthRightLine(), new ArgonAccuracyCounter(), new ArgonComboCounter(), new BarHitErrorMeter(), From d0fea381b18b5f19450f4d93951e0aed152a85c8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Nov 2023 02:13:16 +0300 Subject: [PATCH 594/896] Update skin deserialisation archives --- .../Archives/modified-argon-20231108.osk | Bin 0 -> 1494 bytes .../Archives/modified-argon-pro-20231105.osk | Bin 1899 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk delete mode 100644 osu.Game.Tests/Resources/Archives/modified-argon-pro-20231105.osk diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk new file mode 100644 index 0000000000000000000000000000000000000000..d07c5171007777784216e3ee8d4bc631a602bfb9 GIT binary patch literal 1494 zcmWIWW@Zs#U|`^2Xcf!|uh^e_WIvFX&Il4=U?|Sc%+t%v%RJg|n0LrQ#O1f`(IcWB z8+rX$ChiU?2~qTQ`Ixx>ZD`s5{%hHhb97nVPl+aV1Qjyunwa+G%d_XdW2RNSmguco zzj(5k`_vD>CNlYplEjTyH)@5XT{4unf(89(awmw8vmIBP&~Qzl#P2f&~x=b%nS8oW?ovp zURH5_-s)#T0Rf-9&-jLJ3JN%N*8il>X&>K^jABXeXnw#J9p;v=`-Hf zi&-Q&AHCE#pfYtw79*;wbc&?>&M`4CREjY$01<<4VrHI4sEc!cZb5!tYF4{*mY&!tVq5qJ#(U44BM31-%tO!8+D@iZlx95 z;!KaJH{@S%yxA^7zUQ(M1oD5&e1swB0b=fasoewEjYwjZ}S{?2x8 z#oJS#uHWPPSTxDSGK`AeX9?qUOa!LwkY@Svh^=Z%I-Wb)B1hq+%XCJ$EVJ$ zXJ+R)^s8?Q`@FZ`YqsyU+ZcQMZIx{P^0%Jbxpq(bqF;3>XVb|kyq~4n>fg2{u8WTg z*pbcVGvm_w&aOti$Nzpxo&Uf7S(v20+?36GZ~CL=0GX*tRcgSLuL;C_Kpc>hSecfY znv;Uam_?diI{rSVJingtJ{`U(Xa!prqwD8x@5VVVA{5q7TP?;A;LXS+!hpL#0~!kf zjUWnEyrJttFCn0M7#JF#LUqB*3v{jM*%+a<8d#=c&(!ET7HO literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231105.osk b/osu.Game.Tests/Resources/Archives/modified-argon-pro-20231105.osk deleted file mode 100644 index 16f89502eff0bc46df9922fec370ee1d804170f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1899 zcmWIWW@Zs#U|`^2U=>IWU(2a$c^JquWCDpWFcfEJ=ILeTWuEQdo5y4*&{A!Gw1sPl z%y(f;r;?D8l}@c1Vtd%dzP&s7{PN3ry-NL4!aF>tXnFaYU#gL+{apE5!+TG>#M-L$ zizgfVX?3b5E|D`);hl1)bI*K-YnzT1HhnuR{6^sY4(_?fzVs-6-5KKe+J>9etx|rg zjG)EDr7qr^?{6wlsW-@<8S$Uztj4_1v=@t#?uGbnK47z=bZzo(gV~C)ChIm%Uj0Jj zn9UA@Z~Hg?IM(GODp|EM?)BA&2jBf!92Fiq*LK&-{&(U5C>}LoaOf%ndaxUad7&Q7 z%uCDH%PP*#Gwm_tI^-bW`g=~&Osm~FN$KihlhimKF^X;GbUrcPAW~59{krBG`~Lr{ z4m}q*f0|FVdpD0wkzCCs&j;b6_m7obKXPNo>^1(Y&e~=9xnAK5KQn>7ZqMN`A+F_z zn)nvaP`qtBXW!3)nUV=U4-?*Q?rOaCw|vz-c~l36?qoWA5f}^S6c`x9fDZIc%*^u$ zb#czmEy&MH%_}JeyY%hM`}wyF1diYT9qwY|?9y<_sI$E<@3o1qbM|iAkkBVHvL9W& z@#2lc;#L2D&vDQU@RyFAtT5|C!>7I7^|j~Ht#{fN%(m=?xLsCSyCL!Zy#JO{IqVZfoDx4sx10T08KL%XUT@v|qaQxInN8Sup`q1r zv(f8mHmUJ5Z)W{9ZYyX`i>b9#4B4#m+RJ+OjJ=PQ(w=cHH58g9;b^v#aV$GrthTgy>^{vX^<~pp_~0Hd#~SvR7WADnJ(QoPE5=_ zcC{e#jaOIQ~!qITd~rQOiRVTX)KldmQ%dB_Rh`6=7me;zH9BN zd~kQYbJfu|_A}p}Q}6wIc-yU}yZ@F2F1@wq=k)(wds_O#^O=kL&KSiwvu#P5_V|hq z`_1_q1E zzyL``1_onbM&Ji#g5dnT^x)K-)Z`Ly>d&3%pMBVX=ji*tBFCO5gqdwKdXbn?FJZlA z&0W1zcJ)x<;83n@hoijq{eCkcHIvhB#`nozzfABKR(!Jfs=bwS#ZukV)rDL-7nm$B zOMKaNb Date: Wed, 8 Nov 2023 02:13:35 +0300 Subject: [PATCH 595/896] Remove unused local --- osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs b/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs index fe495b421f..e8ade9e5c6 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreWedge.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.Graphics; using osu.Framework.Graphics.Containers; @@ -19,8 +18,6 @@ namespace osu.Game.Screens.Play.HUD { AutoSizeAxes = Axes.Both; - var vertices = new List(); - InternalChildren = new Drawable[] { new ArgonWedgePiece From 387de7ec244f1e5c7381693b6dad96a64630ce1f Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 7 Nov 2023 15:53:41 -0800 Subject: [PATCH 596/896] Add ability to view kudosu rankings --- .../API/Requests/GetKudosuRankingsRequest.cs | 28 ++++++ .../API/Requests/GetKudosuRankingsResponse.cs | 15 +++ osu.Game/Overlays/KudosuTable.cs | 95 +++++++++++++++++++ osu.Game/Overlays/Rankings/RankingsScope.cs | 5 +- osu.Game/Overlays/RankingsOverlay.cs | 9 ++ 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/API/Requests/GetKudosuRankingsRequest.cs create mode 100644 osu.Game/Online/API/Requests/GetKudosuRankingsResponse.cs create mode 100644 osu.Game/Overlays/KudosuTable.cs diff --git a/osu.Game/Online/API/Requests/GetKudosuRankingsRequest.cs b/osu.Game/Online/API/Requests/GetKudosuRankingsRequest.cs new file mode 100644 index 0000000000..cd361bf7b8 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetKudosuRankingsRequest.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.IO.Network; + +namespace osu.Game.Online.API.Requests +{ + public class GetKudosuRankingsRequest : APIRequest + { + private readonly int page; + + public GetKudosuRankingsRequest(int page = 1) + { + this.page = page; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.AddParameter(@"page", page.ToString()); + + return req; + } + + protected override string Target => @"rankings/kudosu"; + } +} diff --git a/osu.Game/Online/API/Requests/GetKudosuRankingsResponse.cs b/osu.Game/Online/API/Requests/GetKudosuRankingsResponse.cs new file mode 100644 index 0000000000..4e3ade3795 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetKudosuRankingsResponse.cs @@ -0,0 +1,15 @@ +// 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 Newtonsoft.Json; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetKudosuRankingsResponse + { + [JsonProperty("ranking")] + public List Users = null!; + } +} diff --git a/osu.Game/Overlays/KudosuTable.cs b/osu.Game/Overlays/KudosuTable.cs new file mode 100644 index 0000000000..93884435a4 --- /dev/null +++ b/osu.Game/Overlays/KudosuTable.cs @@ -0,0 +1,95 @@ +// 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.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.Rankings.Tables; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Users; + +namespace osu.Game.Overlays +{ + public partial class KudosuTable : RankingsTable + { + public KudosuTable(int page, List users) + : base(page, users) + { + } + + protected override Drawable CreateRowBackground(APIUser item) + { + var background = base.CreateRowBackground(item); + + // see: https://github.com/ppy/osu-web/blob/9de00a0b874c56893d98261d558d78d76259d81b/resources/views/multiplayer/rooms/_rankings_table.blade.php#L23 + if (!item.Active) + background.Alpha = 0.5f; + + return background; + } + + protected override Drawable[] CreateRowContent(int index, APIUser item) + { + var content = base.CreateRowContent(index, item); + + // see: https://github.com/ppy/osu-web/blob/9de00a0b874c56893d98261d558d78d76259d81b/resources/views/multiplayer/rooms/_rankings_table.blade.php#L23 + if (!item.Active) + { + foreach (var d in content) + d.Alpha = 0.5f; + } + + return content; + } + + protected override RankingsTableColumn[] CreateAdditionalHeaders() + { + const int min_width = 120; + return new[] + { + new RankingsTableColumn(RankingsStrings.KudosuTotal, Anchor.Centre, new Dimension(GridSizeMode.AutoSize, minSize: min_width), true), + new RankingsTableColumn(RankingsStrings.KudosuAvailable, Anchor.Centre, new Dimension(GridSizeMode.AutoSize, minSize: min_width)), + new RankingsTableColumn(RankingsStrings.KudosuUsed, Anchor.Centre, new Dimension(GridSizeMode.AutoSize, minSize: min_width)), + }; + } + + protected override Drawable[] CreateAdditionalContent(APIUser item) + { + int kudosuTotal = item.Kudosu.Total; + int kudosuAvailable = item.Kudosu.Available; + return new Drawable[] + { + new RowText + { + Text = kudosuTotal.ToLocalisableString(@"N0") + }, + new ColouredRowText + { + Text = kudosuAvailable.ToLocalisableString(@"N0") + }, + new ColouredRowText + { + Text = (kudosuTotal - kudosuAvailable).ToLocalisableString(@"N0") + }, + }; + } + + protected override CountryCode GetCountryCode(APIUser item) => item.CountryCode; + + protected override Drawable CreateFlagContent(APIUser item) + { + var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + TextAnchor = Anchor.CentreLeft + }; + username.AddUserLink(item); + return username; + } + } +} diff --git a/osu.Game/Overlays/Rankings/RankingsScope.cs b/osu.Game/Overlays/Rankings/RankingsScope.cs index 3392db9360..356a861764 100644 --- a/osu.Game/Overlays/Rankings/RankingsScope.cs +++ b/osu.Game/Overlays/Rankings/RankingsScope.cs @@ -18,6 +18,9 @@ namespace osu.Game.Overlays.Rankings Score, [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeCountry))] - Country + Country, + + [LocalisableDescription(typeof(RankingsStrings), nameof(RankingsStrings.TypeKudosu))] + Kudosu, } } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index f25bf80b6a..6a32515cbc 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -135,6 +135,9 @@ namespace osu.Game.Overlays case RankingsScope.Score: return new GetUserRankingsRequest(ruleset.Value, UserRankingsType.Score); + + case RankingsScope.Kudosu: + return new GetKudosuRankingsRequest(); } return null; @@ -166,6 +169,12 @@ namespace osu.Game.Overlays return new CountriesTable(1, countryRequest.Response.Countries); } + + case GetKudosuRankingsRequest kudosuRequest: + if (kudosuRequest.Response == null) + return null; + + return new KudosuTable(1, kudosuRequest.Response.Users); } return null; From c8d276281ada839a37ab6eb484c36b9b7772a0d1 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 7 Nov 2023 15:49:11 -0800 Subject: [PATCH 597/896] Fix flags not showing on kudosu rankings The `country` attribute is optional and not included in the kudosu rankings response so use `country_code` instead. --- osu.Game/Online/API/Requests/Responses/APIUser.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs index 7c4093006d..2ee66453cf 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -34,20 +34,15 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"previous_usernames")] public string[] PreviousUsernames; - private CountryCode? countryCode; + [JsonProperty(@"country_code")] + private string countryCodeString; public CountryCode CountryCode { - get => countryCode ??= (Enum.TryParse(country?.Code, out CountryCode result) ? result : default); - set => countryCode = value; + get => Enum.TryParse(countryCodeString, out CountryCode result) ? result : CountryCode.Unknown; + set => countryCodeString = value.ToString(); } -#pragma warning disable 649 - [CanBeNull] - [JsonProperty(@"country")] - private Country country; -#pragma warning restore 649 - public readonly Bindable Status = new Bindable(); public readonly Bindable Activity = new Bindable(); From 6c6baab1156f64388e5fcea3ad712cc7e24bb463 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Nov 2023 16:41:30 +0900 Subject: [PATCH 598/896] Reword comment to explain why --- osu.Game/Graphics/UserInterface/BarGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index d3eebd71f0..0ac987e85b 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -148,7 +148,7 @@ namespace osu.Game.Graphics.UserInterface if (barHeight == 0 || barWidth == 0) continue; - // Make sure draw quad is thick enough + // Apply minimum sizing to hide the fact that we don't have fractional anti-aliasing. barHeight = Math.Max(barHeight, 1.5f); barWidth = Math.Max(barWidth, 1.5f); From 38847c3ac5383c9fb69b7bf0b38aa8ea36b23b39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 8 Nov 2023 17:23:01 +0900 Subject: [PATCH 599/896] Change test to move only on a toggle step --- osu.Game.Tests/Visual/Online/TestSceneGraph.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs index eee29e0aeb..f4bde159e5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs @@ -13,10 +13,10 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public partial class TestSceneGraph : OsuTestScene { - private readonly BarGraph graph; - public TestSceneGraph() { + BarGraph graph; + Child = graph = new BarGraph { RelativeSizeAxes = Axes.Both, @@ -34,13 +34,14 @@ namespace osu.Game.Tests.Visual.Online AddStep("Top to bottom", () => graph.Direction = BarDirection.TopToBottom); AddStep("Left to right", () => graph.Direction = BarDirection.LeftToRight); AddStep("Right to left", () => graph.Direction = BarDirection.RightToLeft); - } - protected override void LoadComplete() - { - base.LoadComplete(); - - graph.MoveToY(-10, 1000).Then().MoveToY(10, 1000).Loop(); + AddToggleStep("Toggle movement", enabled => + { + if (enabled) + graph.MoveToY(-10, 1000).Then().MoveToY(10, 1000).Loop(); + else + graph.ClearTransforms(); + }); } } } From fc1a0cf645b345c1b2b0727313a4f11b207a3268 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 19 Oct 2023 16:20:10 +0900 Subject: [PATCH 600/896] Update `ButtonSystem` to use new sample names --- osu.Game/Screens/Menu/ButtonSystem.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index bf2eba43c0..d26709151c 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -127,14 +127,14 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader(true)] private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) { - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); - buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); + buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); if (host.CanExit) buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); From 17aa079cb1cd1ee7ae22942aa582507c37037c4d Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 8 Nov 2023 22:08:05 +0900 Subject: [PATCH 601/896] Use new tiered 'back' samples --- osu.Game/Screens/Menu/ButtonSystem.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index d26709151c..8ebe4de8de 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Menu private readonly List buttonsTopLevel = new List(); private readonly List buttonsPlay = new List(); - private Sample sampleBack; + private Sample sampleBackToLogo; private readonly LogoTrackingContainer logoTrackingContainer; @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(new Drawable[] { new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) { VisibleState = ButtonSystemState.Play, @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Menu if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); - sampleBack = audio.Samples.Get(@"Menu/button-back-select"); + sampleBackToLogo = audio.Samples.Get(@"Menu/back-to-logo"); } private void onMultiplayer() @@ -260,7 +260,9 @@ namespace osu.Game.Screens.Menu { case ButtonSystemState.TopLevel: State = ButtonSystemState.Initial; - sampleBack?.Play(); + + // Samples are explicitly played here in response to user interaction and not when transitioning due to idle. + sampleBackToLogo?.Play(); return true; case ButtonSystemState.Play: From f0a1df06aced3051a93684588a6f21a0bf0fafd0 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 8 Nov 2023 22:13:35 +0900 Subject: [PATCH 602/896] Add 'swoosh' samples to accentuate `MainMenu` animations --- osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++++ osu.Game/Screens/Menu/MainMenu.cs | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 8ebe4de8de..20af7e4d4a 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -86,6 +86,7 @@ namespace osu.Game.Screens.Menu private readonly List buttonsPlay = new List(); private Sample sampleBackToLogo; + private Sample sampleLogoSwoosh; private readonly LogoTrackingContainer logoTrackingContainer; @@ -156,6 +157,7 @@ namespace osu.Game.Screens.Menu if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); sampleBackToLogo = audio.Samples.Get(@"Menu/back-to-logo"); + sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh"); } private void onMultiplayer() @@ -263,6 +265,8 @@ namespace osu.Game.Screens.Menu // Samples are explicitly played here in response to user interaction and not when transitioning due to idle. sampleBackToLogo?.Play(); + sampleLogoSwoosh?.Play(); + return true; case ButtonSystemState.Play: diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 22040b4f0b..36e336e960 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -7,6 +7,8 @@ using System; using System.Diagnostics; using JetBrains.Annotations; 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.Containers; @@ -89,8 +91,10 @@ namespace osu.Game.Screens.Menu private SongTicker songTicker; private Container logoTarget; + private Sample reappearSampleSwoosh; + [BackgroundDependencyLoader(true)] - private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) + private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics, AudioManager audio) { holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); loginDisplayed = statics.GetBindable(Static.LoginOverlayDisplayed); @@ -162,6 +166,8 @@ namespace osu.Game.Screens.Menu Buttons.OnSettings = () => settings?.ToggleVisibility(); Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); + reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh"); + preloadSongSelect(); } @@ -291,6 +297,8 @@ namespace osu.Game.Screens.Menu { base.OnResuming(e); + reappearSampleSwoosh?.Play(); + ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next()); // we may have consumed our preloaded instance, so let's make another. From f69c2ea39b46fff223c8372e6dd1d2a6b2da5bce Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 8 Nov 2023 22:18:33 +0900 Subject: [PATCH 603/896] Implement sample choking/muting for `ButtonSystem` samples --- osu.Game/Screens/Menu/ButtonSystem.cs | 10 ++++++++++ osu.Game/Screens/Menu/MainMenu.cs | 2 ++ osu.Game/Screens/Menu/MainMenuButton.cs | 6 +++++- osu.Game/Screens/Menu/OsuLogo.cs | 10 +++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 20af7e4d4a..13464d4927 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -199,6 +199,7 @@ namespace osu.Game.Screens.Menu { if (State == ButtonSystemState.Initial) { + StopSamplePlayback(); logo?.TriggerClick(); return true; } @@ -264,12 +265,14 @@ namespace osu.Game.Screens.Menu State = ButtonSystemState.Initial; // Samples are explicitly played here in response to user interaction and not when transitioning due to idle. + StopSamplePlayback(); sampleBackToLogo?.Play(); sampleLogoSwoosh?.Play(); return true; case ButtonSystemState.Play: + StopSamplePlayback(); backButton.TriggerClick(); return true; @@ -278,6 +281,13 @@ namespace osu.Game.Screens.Menu } } + public void StopSamplePlayback() + { + buttonsPlay.ForEach(button => button.StopSamplePlayback()); + buttonsTopLevel.ForEach(button => button.StopSamplePlayback()); + logo?.StopSamplePlayback(); + } + private bool onOsuLogo() { switch (state) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 36e336e960..0f73707544 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -297,6 +297,8 @@ namespace osu.Game.Screens.Menu { base.OnResuming(e); + // Ensures any playing `ButtonSystem` samples are stopped when returning to MainMenu (as to not overlap with the 'back' sample) + Buttons.StopSamplePlayback(); reappearSampleSwoosh?.Play(); ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next()); diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index c3a96e36a1..63fc34b4fb 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -51,6 +51,7 @@ namespace osu.Game.Screens.Menu private readonly Action clickAction; private Sample sampleClick; private Sample sampleHover; + private SampleChannel sampleChannel; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); @@ -225,7 +226,8 @@ namespace osu.Game.Screens.Menu private void trigger() { - sampleClick?.Play(); + sampleChannel = sampleClick?.GetChannel(); + sampleChannel?.Play(); clickAction?.Invoke(); @@ -237,6 +239,8 @@ namespace osu.Game.Screens.Menu public override bool HandleNonPositionalInput => state == ButtonState.Expanded; public override bool HandlePositionalInput => state != ButtonState.Exploded && box.Scale.X >= 0.8f; + public void StopSamplePlayback() => sampleChannel?.Stop(); + protected override void Update() { iconText.Alpha = Math.Clamp((box.Scale.X - 0.5f) / 0.3f, 0, 1); diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 8867ecfb2a..75ef8be02e 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -52,6 +52,8 @@ namespace osu.Game.Screens.Menu private readonly IntroSequence intro; private Sample sampleClick; + private SampleChannel sampleClickChannel; + private Sample sampleBeat; private Sample sampleDownbeat; @@ -391,7 +393,11 @@ namespace osu.Game.Screens.Menu flashLayer.FadeOut(1500, Easing.OutExpo); if (Action?.Invoke() == true) - sampleClick.Play(); + { + StopSamplePlayback(); + sampleClickChannel = sampleClick.GetChannel(); + sampleClickChannel.Play(); + } return true; } @@ -440,6 +446,8 @@ namespace osu.Game.Screens.Menu private Container currentProxyTarget; private Drawable proxy; + public void StopSamplePlayback() => sampleClickChannel?.Stop(); + public Drawable ProxyToContainer(Container c) { if (currentProxyTarget != null) From 12a148d1084b1a6f11525ff16c223ecb6ddd6927 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Nov 2023 12:26:41 +0900 Subject: [PATCH 604/896] Use new initialisation structure for github sourced update manager --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 941ab335e8..dba157a6e9 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -10,8 +10,8 @@ using osu.Game; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; -using Squirrel; using Squirrel.SimpleSplat; +using Squirrel.Sources; using LogLevel = Squirrel.SimpleSplat.LogLevel; using UpdateManager = osu.Game.Updater.UpdateManager; @@ -63,7 +63,7 @@ namespace osu.Desktop.Updater if (localUserInfo?.IsPlaying.Value == true) return false; - updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer"); + updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer"); var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); From 8a47f05b1615e126c9841b52dd63576a5b1b521a Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 9 Nov 2023 14:01:33 +0900 Subject: [PATCH 605/896] Always play 'swoosh' sample when transitioning back to logo --- osu.Game/Screens/Menu/ButtonSystem.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 13464d4927..a0cf9f5322 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -267,7 +267,6 @@ namespace osu.Game.Screens.Menu // Samples are explicitly played here in response to user interaction and not when transitioning due to idle. StopSamplePlayback(); sampleBackToLogo?.Play(); - sampleLogoSwoosh?.Play(); return true; @@ -362,6 +361,9 @@ namespace osu.Game.Screens.Menu logo?.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo); logo?.ScaleTo(1, 800, Easing.OutExpo); }, buttonArea.Alpha * 150); + + if (lastState == ButtonSystemState.TopLevel) + sampleLogoSwoosh?.Play(); break; case ButtonSystemState.TopLevel: From 5d5c803cf6fc97c044551a8bd7d3d11f0db8f41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Nov 2023 17:48:43 +0900 Subject: [PATCH 606/896] Add failing test case for touch device application during gameplay with incompatible mods active --- .../Mods/TestSceneOsuModTouchDevice.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index e41cc8cfbc..abeb56209e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; using osu.Game.Configuration; using osu.Game.Input; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -113,6 +114,25 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); } + [Test] + public void TestIncompatibleModActive() + { + // this is only a veneer of enabling autopilot as having it actually active from the start is annoying to make happen + // given the tests' structure. + AddStep("enable autopilot", () => Player.Score.ScoreInfo.Mods = new Mod[] { new OsuModAutopilot() }); + + AddUntilStep("wait until 0 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0).Within(500)); + AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); + AddUntilStep("wait until 0", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + AddStep("touch playfield", () => + { + var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.BeginTouch(touch); + InputManager.EndTouch(touch); + }); + AddAssert("touch device mod not activated", () => Player.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); + } + [Test] public void TestSecondObjectTouched() { From 63a0ea54109c93984501b66435febf676663cab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 9 Nov 2023 17:50:57 +0900 Subject: [PATCH 607/896] Fix `PlayerTouchInputDetector` applying touch device mod when other inactive mods present --- osu.Game/Screens/Play/PlayerTouchInputDetector.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs index a5055dfb00..69c3cd0ded 100644 --- a/osu.Game/Screens/Play/PlayerTouchInputDetector.cs +++ b/osu.Game/Screens/Play/PlayerTouchInputDetector.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Utils; namespace osu.Game.Screens.Play { @@ -45,10 +46,15 @@ namespace osu.Game.Screens.Play if (touchDeviceMod == null) return; + var candidateMods = player.Score.ScoreInfo.Mods.Append(touchDeviceMod).ToArray(); + + if (!ModUtils.CheckCompatibleSet(candidateMods, out _)) + return; + // `Player` (probably rightly so) assumes immutability of mods, // so this will not be shown immediately on the mod display in the top right. // if this is to change, the mod immutability should be revisited. - player.Score.ScoreInfo.Mods = player.Score.ScoreInfo.Mods.Append(touchDeviceMod).ToArray(); + player.Score.ScoreInfo.Mods = candidateMods; } } } From e3e752b912ba0b14f9d2d2a96bc1695e65eb94cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Nov 2023 17:56:58 +0900 Subject: [PATCH 608/896] 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 60c71a736d..73cd239854 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From ccb9ff826a87dbafbbeb0a0a572a404bc88c7e88 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Thu, 9 Nov 2023 13:09:59 +0100 Subject: [PATCH 609/896] fixed tests --- .../Online/TestSceneUserClickableAvatar.cs | 130 +++++++++--------- .../OnlinePlay/Components/ParticipantsList.cs | 2 +- .../DrawableRoomParticipantsList.cs | 2 +- osu.Game/Users/Drawables/ClickableAvatar.cs | 115 +++++++++++----- osu.Game/Users/Drawables/UpdateableAvatar.cs | 9 +- 5 files changed, 152 insertions(+), 106 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 678767f15e..9217104aa8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -1,11 +1,13 @@ // 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.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Testing; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -28,8 +30,10 @@ namespace osu.Game.Tests.Visual.Online Children = new[] { generateUser(@"peppy", 2, CountryCode.AU, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false, "99EB47"), - generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", false), - generateUser(@"joshika39", 17032217, CountryCode.RS, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", true), + generateUser(@"flyte", 3103765, CountryCode.JP, @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", true), + generateUser(@"joshika39", 17032217, CountryCode.RS, @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", false), + new UpdateableAvatar(), + new UpdateableAvatar() }, }; }); @@ -37,68 +41,68 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestClickableAvatarHover() { - // AddStep($"click {1}. {nameof(ClickableAvatar)}", () => - // { - // var targets = this.ChildrenOfType().ToList(); - // if (targets.Count < 1) - // return; - // - // InputManager.MoveMouseTo(targets[0]); - // }); - // AddWaitStep("wait for tooltip to show", 5); - // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - // AddWaitStep("wait for tooltip to hide", 3); - // - // AddStep($"click {2}. {nameof(ClickableAvatar)}", () => - // { - // var targets = this.ChildrenOfType().ToList(); - // if (targets.Count < 2) - // return; - // - // InputManager.MoveMouseTo(targets[1]); - // }); - // AddWaitStep("wait for tooltip to show", 5); - // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - // AddWaitStep("wait for tooltip to hide", 3); - // - // AddStep($"click {3}. {nameof(ClickableAvatar)}", () => - // { - // var targets = this.ChildrenOfType().ToList(); - // if (targets.Count < 3) - // return; - // - // InputManager.MoveMouseTo(targets[2]); - // }); - // AddWaitStep("wait for tooltip to show", 5); - // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - // AddWaitStep("wait for tooltip to hide", 3); - // - // AddStep($"click null user {4}. {nameof(ClickableAvatar)}", () => - // { - // var targets = this.ChildrenOfType().ToList(); - // if (targets.Count < 4) - // return; - // - // InputManager.MoveMouseTo(targets[3]); - // }); - // AddWaitStep("wait for tooltip to show", 5); - // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - // AddWaitStep("wait for tooltip to hide", 3); - // - // AddStep($"click null user {5}. {nameof(ClickableAvatar)}", () => - // { - // var targets = this.ChildrenOfType().ToList(); - // if (targets.Count < 5) - // return; - // - // InputManager.MoveMouseTo(targets[4]); - // }); - // AddWaitStep("wait for tooltip to show", 5); - // AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - // AddWaitStep("wait for tooltip to hide", 3); + AddStep($"click user {1} with UserGridPanel {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 1) + return; + + InputManager.MoveMouseTo(targets[0]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); + + AddStep($"click user {2} with username only. {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 2) + return; + + InputManager.MoveMouseTo(targets[1]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); + + AddStep($"click user {3} with UserGridPanel {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 3) + return; + + InputManager.MoveMouseTo(targets[2]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); + + AddStep($"click null user {4}. {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 4) + return; + + InputManager.MoveMouseTo(targets[3]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); + + AddStep($"click null user {5}. {nameof(ClickableAvatar)}", () => + { + var targets = this.ChildrenOfType().ToList(); + if (targets.Count < 5) + return; + + InputManager.MoveMouseTo(targets[4]); + }); + AddWaitStep("wait for tooltip to show", 5); + AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddWaitStep("wait for tooltip to hide", 3); } - private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool isTooltipEnabled, string? color = null) + private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool onlyUsername, string? color = null) { return new ClickableAvatar(new APIUser { @@ -121,7 +125,7 @@ namespace osu.Game.Tests.Visual.Online { Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), }, - IsTooltipEnabled = isTooltipEnabled, + ShowUsernameOnly = onlyUsername, }; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index cb1a846d6c..8cde7859b2 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Components RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex(@"27252d"), }, - avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }, + avatar = new UpdateableAvatar(showUsernameOnly: true) { RelativeSizeAxes = Axes.Both }, }; } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 1814f5359f..65f0555612 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -289,7 +289,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components set => avatar.User = value; } - private readonly UpdateableAvatar avatar = new UpdateableAvatar { RelativeSizeAxes = Axes.Both }; + private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameOnly: true) { RelativeSizeAxes = Axes.Both }; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 376ce0b821..e7934016bc 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -1,44 +1,43 @@ // 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.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; -using osu.Game.Localisation; +using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; using osuTK; +using osuTK.Graphics; namespace osu.Game.Users.Drawables { - public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip + public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip { - public ITooltip GetCustomTooltip() - { - return new APIUserTooltip(user); - } + // public ITooltip GetCustomTooltip() => new APIUserTooltip(user!) { ShowTooltip = TooltipEnabled }; + public ITooltip GetCustomTooltip() => new APIUserTooltip(new APIUserTooltipContent(user!)); - public APIUser? TooltipContent => user; - - public override LocalisableString TooltipText - { - get - { - if (!Enabled.Value) - return string.Empty; - - return !IsTooltipEnabled ? (user?.Username ?? string.Empty) : ContextMenuStrings.ViewProfile; - } - set => throw new NotSupportedException(); - } - - public bool IsTooltipEnabled { get; set; } + public APIUserTooltipContent TooltipContent => content; + private readonly APIUserTooltipContent content; private readonly APIUser? user; + private bool tooltipEnabled; + + public override LocalisableString TooltipText => user!.Username; + + public bool ShowUsernameOnly + { + get => tooltipEnabled; + set + { + tooltipEnabled = value; + content.ShowUsernameOnly = ShowUsernameOnly; + } + } [Resolved] private OsuGame? game { get; set; } @@ -53,11 +52,8 @@ namespace osu.Game.Users.Drawables if (user?.Id != APIUser.SYSTEM_USER_ID) Action = openProfile; - } - public void SetValue(out bool value) - { - value = IsTooltipEnabled; + content = new APIUserTooltipContent(user!, ShowUsernameOnly); } [BackgroundDependencyLoader] @@ -80,23 +76,57 @@ namespace osu.Game.Users.Drawables return base.OnClick(e); } - public partial class APIUserTooltip : VisibilityContainer, ITooltip + public partial class APIUserTooltip : VisibilityContainer, ITooltip { - private APIUser? user; - - public APIUserTooltip(APIUser? user) + private OsuSpriteText text; + private APIUserTooltipContent content; + public APIUserTooltip(APIUserTooltipContent content) { - this.user = user; + this.content = content; + AutoSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 5; + + Child = new UserGridPanel(content.User) + { + Width = 300 + }; + text = new OsuSpriteText() + { + Text = this.content.User.Username + }; } protected override void PopIn() { - if (user is null) + if (content.ShowUsernameOnly) { - return; + Child = new UserGridPanel(content.User) + { + Width = 300 + }; + } + else + { + Alpha = 0; + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray, + }, + text = new OsuSpriteText() + { + Font = FrameworkFont.Regular.With(size: 16), + Padding = new MarginPadding(5), + Text = content.User.Username + } + }; } - Child = new UserGridPanel(user); this.FadeIn(20, Easing.OutQuint); } @@ -104,9 +134,22 @@ namespace osu.Game.Users.Drawables public void Move(Vector2 pos) => Position = pos; - public void SetContent(APIUser user) + public void SetContent(APIUserTooltipContent content) { - this.user = user; + this.content = content; + text.Text = this.content.User.Username; + } + } + + public class APIUserTooltipContent + { + public APIUser User { get; } + public bool ShowUsernameOnly { get; set; } + + public APIUserTooltipContent(APIUser user, bool showUsernameOnly = false) + { + User = user; + ShowUsernameOnly = showUsernameOnly; } } } diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 64d64c56ce..f6363f61e6 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -47,20 +47,20 @@ namespace osu.Game.Users.Drawables private readonly bool isInteractive; private readonly bool showGuestOnNull; - private readonly bool showUserPanel; + private readonly bool showUsernameOnly; /// /// Construct a new UpdateableAvatar. /// /// The initial user to display. /// If set to true, hover/click sounds will play and clicking the avatar will open the user's profile. - /// If set to true, the user status panel will be displayed in the tooltip. + /// If set to true, the user status panel will be displayed in the tooltip. /// Whether to show a default guest representation on null user (as opposed to nothing). - public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUserPanel = true, bool showGuestOnNull = true) + public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUsernameOnly = false, bool showGuestOnNull = true) { this.isInteractive = isInteractive; this.showGuestOnNull = showGuestOnNull; - this.showUserPanel = showUserPanel; + this.showUsernameOnly = showUsernameOnly; User = user; } @@ -75,7 +75,6 @@ namespace osu.Game.Users.Drawables return new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, - IsTooltipEnabled = showUserPanel }; } else From 4900a91c60fda24f76aeba4d2d2bf42cc1929e2d Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Thu, 9 Nov 2023 13:27:09 +0100 Subject: [PATCH 610/896] fixed static analysis problems and finished the implementation --- .../Online/TestSceneUserClickableAvatar.cs | 40 +++++++++---------- osu.Game/Users/Drawables/ClickableAvatar.cs | 12 +++--- osu.Game/Users/Drawables/UpdateableAvatar.cs | 11 +++-- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 9217104aa8..50e5653ad5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -105,28 +105,28 @@ namespace osu.Game.Tests.Visual.Online private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool onlyUsername, string? color = null) { return new ClickableAvatar(new APIUser + { + Username = username, + Id = id, + CountryCode = countryCode, + CoverUrl = cover, + Colour = color ?? "000000", + Status = { - Username = username, - Id = id, - CountryCode = countryCode, - CoverUrl = cover, - Colour = color ?? "000000", - Status = - { - Value = new UserStatusOnline() - }, - }) + Value = new UserStatusOnline() + }, + }) + { + Width = 50, + Height = 50, + CornerRadius = 10, + Masking = true, + EdgeEffect = new EdgeEffectParameters { - Width = 50, - Height = 50, - CornerRadius = 10, - Masking = true, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), - }, - ShowUsernameOnly = onlyUsername, - }; + Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), + }, + ShowUsernameOnly = onlyUsername, + }; } } } diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index e7934016bc..de0bcad497 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -21,9 +21,8 @@ namespace osu.Game.Users.Drawables // public ITooltip GetCustomTooltip() => new APIUserTooltip(user!) { ShowTooltip = TooltipEnabled }; public ITooltip GetCustomTooltip() => new APIUserTooltip(new APIUserTooltipContent(user!)); - public APIUserTooltipContent TooltipContent => content; + public APIUserTooltipContent TooltipContent { get; } - private readonly APIUserTooltipContent content; private readonly APIUser? user; private bool tooltipEnabled; @@ -35,7 +34,7 @@ namespace osu.Game.Users.Drawables set { tooltipEnabled = value; - content.ShowUsernameOnly = ShowUsernameOnly; + TooltipContent.ShowUsernameOnly = ShowUsernameOnly; } } @@ -53,7 +52,7 @@ namespace osu.Game.Users.Drawables if (user?.Id != APIUser.SYSTEM_USER_ID) Action = openProfile; - content = new APIUserTooltipContent(user!, ShowUsernameOnly); + TooltipContent = new APIUserTooltipContent(user!, ShowUsernameOnly); } [BackgroundDependencyLoader] @@ -80,6 +79,7 @@ namespace osu.Game.Users.Drawables { private OsuSpriteText text; private APIUserTooltipContent content; + public APIUserTooltip(APIUserTooltipContent content) { this.content = content; @@ -91,7 +91,7 @@ namespace osu.Game.Users.Drawables { Width = 300 }; - text = new OsuSpriteText() + text = new OsuSpriteText { Text = this.content.User.Username }; @@ -118,7 +118,7 @@ namespace osu.Game.Users.Drawables RelativeSizeAxes = Axes.Both, Colour = Color4.Gray, }, - text = new OsuSpriteText() + text = new OsuSpriteText { Font = FrameworkFont.Regular.With(size: 16), Padding = new MarginPadding(5), diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index f6363f61e6..f220ee5a25 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -75,15 +75,14 @@ namespace osu.Game.Users.Drawables return new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, + ShowUsernameOnly = showUsernameOnly }; } - else + + return new DrawableAvatar(user) { - return new DrawableAvatar(user) - { - RelativeSizeAxes = Axes.Both, - }; - } + RelativeSizeAxes = Axes.Both, + }; } } } From 4fa158e0d832f44cddefac1b0dd7b94f879e3993 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Nov 2023 21:35:37 +0900 Subject: [PATCH 611/896] Split tournament player lists more equally --- .../Components/DrawableTeamWithPlayers.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs index 4f0c7d6b72..fd7a51140b 100644 --- a/osu.Game.Tournament/Components/DrawableTeamWithPlayers.cs +++ b/osu.Game.Tournament/Components/DrawableTeamWithPlayers.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 System; using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -17,6 +19,11 @@ namespace osu.Game.Tournament.Components { AutoSizeAxes = Axes.Both; + var players = team?.Players ?? new BindableList(); + + // split the players into two even columns, favouring the first column if odd. + int split = (int)Math.Ceiling(players.Count / 2f); + InternalChildren = new Drawable[] { new FillFlowContainer @@ -39,13 +46,13 @@ namespace osu.Game.Tournament.Components { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - ChildrenEnumerable = team?.Players.Select(createPlayerText).Take(5) ?? Enumerable.Empty() + ChildrenEnumerable = players.Take(split).Select(createPlayerText), }, new FillFlowContainer { Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - ChildrenEnumerable = team?.Players.Select(createPlayerText).Skip(5) ?? Enumerable.Empty() + ChildrenEnumerable = players.Skip(split).Select(createPlayerText), }, } }, From 1ae3265f925911c3a1f7f640805765b68dc5d335 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Nov 2023 21:24:30 +0900 Subject: [PATCH 612/896] Update tests in line with new behaviour --- .../OsuDifficultyCalculatorTest.cs | 20 +++++------ .../TestSceneSliderInput.cs | 36 ++++++++++++------- .../Rulesets/Scoring/HitResultTest.cs | 2 +- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 7b7deb9c67..1d76c24620 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.710442985146793d, 206, "diffcalc-test")] - [TestCase(1.4386882251130073d, 45, "zero-length-sliders")] - [TestCase(0.42506480230838789d, 2, "very-fast-slider")] - [TestCase(0.14102693012101306d, 1, "nan-slider")] + [TestCase(6.710442985146793d, 239, "diffcalc-test")] + [TestCase(1.4386882251130073d, 54, "zero-length-sliders")] + [TestCase(0.42506480230838789d, 4, "very-fast-slider")] + [TestCase(0.14102693012101306d, 2, "nan-slider")] public void Test(double expectedStarRating, int expectedMaxCombo, string name) => base.Test(expectedStarRating, expectedMaxCombo, name); - [TestCase(8.9742952703071666d, 206, "diffcalc-test")] - [TestCase(0.55071082800473514d, 2, "very-fast-slider")] - [TestCase(1.743180218215227d, 45, "zero-length-sliders")] + [TestCase(8.9742952703071666d, 239, "diffcalc-test")] + [TestCase(0.55071082800473514d, 4, "very-fast-slider")] + [TestCase(1.743180218215227d, 54, "zero-length-sliders")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.710442985146793d, 239, "diffcalc-test")] - [TestCase(0.42506480230838789d, 4, "very-fast-slider")] - [TestCase(1.4386882251130073d, 54, "zero-length-sliders")] + [TestCase(6.710442985146793d, 272, "diffcalc-test")] + [TestCase(0.42506480230838789d, 6, "very-fast-slider")] + [TestCase(1.4386882251130073d, 63, "zero-length-sliders")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index f718a5069f..1e295aae70 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Tests if (hit) assertAllMaxJudgements(); else - AddAssert("Tracking dropped", assertMidSliderJudgementFail); + assertMidSliderJudgementFail(); AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle); @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking lost", assertMidSliderJudgementFail); + assertMidSliderJudgementFail(); } /// @@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider }, }); - AddAssert("Tracking retained, sliderhead miss", assertHeadMissTailTracked); + assertHeadMissTailTracked(); } /// @@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, }); - AddAssert("Tracking re-acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } /// @@ -328,7 +328,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, }); - AddAssert("Tracking lost", assertMidSliderJudgementFail); + assertMidSliderJudgementFail(); } /// @@ -350,7 +350,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, }); - AddAssert("Tracking acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } /// @@ -373,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 }, }); - AddAssert("Tracking acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } [Test] @@ -387,7 +387,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 }, }); - AddAssert("Tracking acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } /// @@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, }); - AddAssert("Tracking acquired", assertMidSliderJudgements); + assertMidSliderJudgements(); } /// @@ -454,7 +454,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.201f), Actions = { OsuAction.LeftButton }, Time = time_slider_end }, }); - AddAssert("Tracking dropped", assertMidSliderJudgementFail); + assertMidSliderJudgementFail(); } private void assertAllMaxJudgements() @@ -465,11 +465,21 @@ namespace osu.Game.Rulesets.Osu.Tests }, () => Is.EqualTo(judgementResults.Select(j => (j.HitObject, j.Judgement.MaxResult)))); } - private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit; + private void assertHeadMissTailTracked() + { + AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit)); + AddAssert("Slider head misseed", () => judgementResults.First().IsHit, () => Is.False); + } - private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit; + private void assertMidSliderJudgements() + { + AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit)); + } - private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; + private void assertMidSliderJudgementFail() + { + AddAssert("Tracking lost", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.IgnoreMiss)); + } private void performTest(List frames, Slider? slider = null, double? bpm = null, int? tickRate = null) { diff --git a/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs index 68d7335055..72acd18c5b 100644 --- a/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })] public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults) { - HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => !minResults.Contains(t)).ToArray(); + HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => t != HitResult.IgnoreMiss && !minResults.Contains(t)).ToArray(); Assert.Multiple(() => { From edef31f426f8568afc3abf53b7611e525308c1d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Nov 2023 21:48:41 +0900 Subject: [PATCH 613/896] Correctly propagate classic behaviour flag to tail --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 7f2d9592af..cb6827b428 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -135,6 +135,8 @@ namespace osu.Game.Rulesets.Osu.Objects classicSliderBehaviour = value; if (HeadCircle != null) HeadCircle.ClassicSliderBehaviour = value; + if (TailCircle != null) + TailCircle.ClassicSliderBehaviour = value; } } @@ -218,6 +220,7 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = e.Time, Position = EndPosition, StackHeight = StackHeight, + ClassicSliderBehaviour = ClassicSliderBehaviour, }); break; From 615d8384abcc3c412a831a70a73ed0f6c6173467 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 9 Nov 2023 22:27:29 +0900 Subject: [PATCH 614/896] Refactor everythign back to sanity --- .../Cards/Statistics/BeatmapCardStatistic.cs | 2 +- osu.Game/Users/Drawables/ClickableAvatar.cs | 93 +++---------------- 2 files changed, 13 insertions(+), 82 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs index 10de2b9128..6fd7142c05 100644 --- a/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs +++ b/osu.Game/Beatmaps/Drawables/Cards/Statistics/BeatmapCardStatistic.cs @@ -74,7 +74,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics #region Tooltip implementation - public virtual ITooltip GetCustomTooltip() => null; + public virtual ITooltip GetCustomTooltip() => null!; public virtual object TooltipContent => null; #endregion diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index de0bcad497..48a12acb5e 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -5,38 +5,25 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Localisation; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osuTK; -using osuTK.Graphics; namespace osu.Game.Users.Drawables { - public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip + public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip { // public ITooltip GetCustomTooltip() => new APIUserTooltip(user!) { ShowTooltip = TooltipEnabled }; - public ITooltip GetCustomTooltip() => new APIUserTooltip(new APIUserTooltipContent(user!)); + public ITooltip GetCustomTooltip() => new UserCardTooltip(); - public APIUserTooltipContent TooltipContent { get; } + public APIUser? TooltipContent { get; } private readonly APIUser? user; - private bool tooltipEnabled; - public override LocalisableString TooltipText => user!.Username; - - public bool ShowUsernameOnly - { - get => tooltipEnabled; - set - { - tooltipEnabled = value; - TooltipContent.ShowUsernameOnly = ShowUsernameOnly; - } - } + // TODO: reimplement. + public bool ShowUsernameOnly { get; set; } [Resolved] private OsuGame? game { get; set; } @@ -47,12 +34,10 @@ namespace osu.Game.Users.Drawables /// The user. A null value will get a placeholder avatar. public ClickableAvatar(APIUser? user = null) { - this.user = user; - if (user?.Id != APIUser.SYSTEM_USER_ID) Action = openProfile; - TooltipContent = new APIUserTooltipContent(user!, ShowUsernameOnly); + TooltipContent = this.user = user; } [BackgroundDependencyLoader] @@ -75,58 +60,17 @@ namespace osu.Game.Users.Drawables return base.OnClick(e); } - public partial class APIUserTooltip : VisibilityContainer, ITooltip + public partial class UserCardTooltip : VisibilityContainer, ITooltip { - private OsuSpriteText text; - private APIUserTooltipContent content; - - public APIUserTooltip(APIUserTooltipContent content) + public UserCardTooltip() { - this.content = content; AutoSizeAxes = Axes.Both; Masking = true; CornerRadius = 5; - - Child = new UserGridPanel(content.User) - { - Width = 300 - }; - text = new OsuSpriteText - { - Text = this.content.User.Username - }; } protected override void PopIn() { - if (content.ShowUsernameOnly) - { - Child = new UserGridPanel(content.User) - { - Width = 300 - }; - } - else - { - Alpha = 0; - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Gray, - }, - text = new OsuSpriteText - { - Font = FrameworkFont.Regular.With(size: 16), - Padding = new MarginPadding(5), - Text = content.User.Username - } - }; - } - this.FadeIn(20, Easing.OutQuint); } @@ -134,23 +78,10 @@ namespace osu.Game.Users.Drawables public void Move(Vector2 pos) => Position = pos; - public void SetContent(APIUserTooltipContent content) + public void SetContent(APIUser? content) => LoadComponentAsync(new UserGridPanel(content ?? new GuestUser()) { - this.content = content; - text.Text = this.content.User.Username; - } - } - - public class APIUserTooltipContent - { - public APIUser User { get; } - public bool ShowUsernameOnly { get; set; } - - public APIUserTooltipContent(APIUser user, bool showUsernameOnly = false) - { - User = user; - ShowUsernameOnly = showUsernameOnly; - } + Width = 300, + }, panel => Child = panel); } } } From 51cf85a9ab3806fe9294d33b24c52165a08aed6b Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 9 Nov 2023 13:22:49 +0100 Subject: [PATCH 615/896] Add touch input settings to android Also updates touch settings so the touch handler can't be disabled on mobile. --- osu.Android/OsuGameAndroid.cs | 4 ++++ .../Settings/Sections/Input/TouchSettings.cs | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index dea70e6b27..52cfb67f42 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers; using osu.Framework.Platform; using osu.Game; using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Updater; using osu.Game.Utils; @@ -97,6 +98,9 @@ namespace osu.Android case AndroidJoystickHandler jh: return new AndroidJoystickSettings(jh); + case AndroidTouchHandler th: + return new TouchSettings(th); + default: return base.CreateSettingsSubsectionFor(handler); } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs index 175fcc4709..0056de6674 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Input.Handlers; using osu.Framework.Localisation; @@ -28,11 +29,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig) { - Add(new SettingsCheckbox + if (!RuntimeInfo.IsMobile) // don't allow disabling the only input method (touch) on mobile. { - LabelText = CommonStrings.Enabled, - Current = handler.Enabled - }); + Add(new SettingsCheckbox + { + LabelText = CommonStrings.Enabled, + Current = handler.Enabled + }); + } Add(new SettingsCheckbox { From 0c4c9aa4b515840611b3b39017ff73d04a3ecf1b Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 9 Nov 2023 14:37:10 +0100 Subject: [PATCH 616/896] Show touch taps setting in player loader on mobile platforms --- osu.Game/Screens/Play/PlayerSettings/InputSettings.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 852fbd8dcc..1387e01305 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.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; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; @@ -22,8 +23,9 @@ namespace osu.Game.Screens.Play.PlayerSettings { new PlayerCheckbox { - LabelText = MouseSettingsStrings.DisableClicksDuringGameplay, - Current = config.GetBindable(OsuSetting.MouseDisableButtons) + // TODO: change to touchscreen detection once https://github.com/ppy/osu/pull/25348 makes it in + LabelText = RuntimeInfo.IsDesktop ? MouseSettingsStrings.DisableClicksDuringGameplay : TouchSettingsStrings.DisableTapsDuringGameplay, + Current = config.GetBindable(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.TouchDisableGameplayTaps) } }; } From 1b08f317fbd1fac55276dc05c29b0339667f2871 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Thu, 9 Nov 2023 15:12:24 +0100 Subject: [PATCH 617/896] Show touch input settings on iOS This does not cover android since `TouchHandler` is SDL-based. --- osu.Game/OsuGameBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1f46eb0c0d..f44dac5f5a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -575,14 +575,14 @@ namespace osu.Game case JoystickHandler jh: return new JoystickSettings(jh); - - case TouchHandler th: - return new TouchSettings(th); } } switch (handler) { + case TouchHandler th: + return new TouchSettings(th); + case MidiHandler: return new InputSection.HandlerSection(handler); From d4722a398892a18f14e868ff471e9132004da8fd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 9 Nov 2023 17:20:05 +0300 Subject: [PATCH 618/896] Add failing test case --- .../TestSceneBackgroundScreenDefault.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index 1523ae7027..e902303505 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -181,6 +181,30 @@ namespace osu.Game.Tests.Visual.Background AddStep("restore default beatmap", () => Beatmap.SetDefault()); } + [Test] + public void TestBeatmapBackgroundWithStoryboardUnloadedOnSuspension() + { + BackgroundScreenBeatmap nestedScreen = null; + + setSupporter(true); + setSourceMode(BackgroundSource.BeatmapWithStoryboard); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithStoryboard()); + AddAssert("background changed", () => screen.CheckLastLoadChange() == true); + AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard)); + + AddUntilStep("storyboard present", () => screen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + + AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); + AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); + + AddUntilStep("storyboard unloaded", () => !screen.ChildrenOfType().Any()); + + AddStep("go back", () => screen.MakeCurrent()); + + AddUntilStep("storyboard reloaded", () => screen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + } + [Test] public void TestBackgroundTypeSwitch() { From bd8409219f079bcdd922ae2169d676608e512364 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 8 Nov 2023 06:37:29 +0300 Subject: [PATCH 619/896] Unload beatmap storyboard background when no longer present --- .../BeatmapBackgroundWithStoryboard.cs | 50 +++++++++++++++++-- osu.Game/Screens/BackgroundScreen.cs | 3 +- osu.Game/Screens/BackgroundScreenStack.cs | 3 ++ .../Backgrounds/BackgroundScreenDefault.cs | 22 ++++++++ 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 9c0d109ce4..e78a93396e 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -1,15 +1,21 @@ // 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.Diagnostics; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; +using osu.Game.Screens; using osu.Game.Storyboards.Drawables; namespace osu.Game.Graphics.Backgrounds @@ -18,6 +24,10 @@ namespace osu.Game.Graphics.Backgrounds { private readonly InterpolatingFramedClock storyboardClock; + private AudioContainer storyboardContainer = null!; + private DrawableStoryboard? drawableStoryboard; + private CancellationTokenSource? loadCancellationSource = new CancellationTokenSource(); + [Resolved(CanBeNull = true)] private MusicController? musicController { get; set; } @@ -33,18 +43,48 @@ namespace osu.Game.Graphics.Backgrounds [BackgroundDependencyLoader] private void load() { + AddInternal(storyboardContainer = new AudioContainer + { + RelativeSizeAxes = Axes.Both, + Volume = { Value = 0 }, + }); + + LoadStoryboard(); + } + + public void LoadStoryboard() + { + Debug.Assert(drawableStoryboard == null); + if (!Beatmap.Storyboard.HasDrawable) return; if (Beatmap.Storyboard.ReplacesBackground) Sprite.Alpha = 0; - LoadComponentAsync(new AudioContainer + LoadComponentAsync(drawableStoryboard = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { - RelativeSizeAxes = Axes.Both, - Volume = { Value = 0 }, - Child = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock } - }, AddInternal); + Clock = storyboardClock + }, s => + { + storyboardContainer.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); + storyboardContainer.Add(s); + }, (loadCancellationSource = new CancellationTokenSource()).Token); + } + + public void UnloadStoryboard(Action scheduleStoryboardRemoval) + { + Debug.Assert(drawableStoryboard != null); + + loadCancellationSource.AsNonNull().Cancel(); + loadCancellationSource = null; + + DrawableStoryboard s = drawableStoryboard; + + storyboardContainer.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); + scheduleStoryboardRemoval(s); + + drawableStoryboard = null; } protected override void LoadComplete() diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index a7502f22d5..73af9b1bf2 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -13,7 +13,8 @@ namespace osu.Game.Screens { public abstract partial class BackgroundScreen : Screen, IEquatable { - protected const float TRANSITION_LENGTH = 500; + public const float TRANSITION_LENGTH = 500; + private const float x_movement_amount = 50; private readonly bool animateOnEnter; diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index 99ca383b9f..3ec1835669 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Storyboards.Drawables; namespace osu.Game.Screens { @@ -33,5 +34,7 @@ namespace osu.Game.Screens base.Push(screen); return true; } + + public void ScheduleStoryboardDisposal(DrawableStoryboard storyboard) => Scheduler.AddDelayed(storyboard.RemoveAndDisposeImmediately, BackgroundScreen.TRANSITION_LENGTH); } } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index d9554c10e2..66835363b4 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -3,11 +3,14 @@ #nullable disable +using System.Diagnostics; using System.Threading; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Framework.Screens; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -71,6 +74,25 @@ namespace osu.Game.Screens.Backgrounds void next() => Next(); } + public override void OnSuspending(ScreenTransitionEvent e) + { + var backgroundScreenStack = Parent as BackgroundScreenStack; + Debug.Assert(backgroundScreenStack != null); + + if (background is BeatmapBackgroundWithStoryboard storyboardBackground) + storyboardBackground.UnloadStoryboard(backgroundScreenStack.ScheduleStoryboardDisposal); + + base.OnSuspending(e); + } + + public override void OnResuming(ScreenTransitionEvent e) + { + if (background is BeatmapBackgroundWithStoryboard storyboardBackground) + storyboardBackground.LoadStoryboard(); + + base.OnResuming(e); + } + private ScheduledDelegate nextTask; private CancellationTokenSource cancellationTokenSource; From 768a31b2f5ec05907f2e1cf18d4bb3f63ae83da1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 9 Nov 2023 22:56:48 +0300 Subject: [PATCH 620/896] Fix background crash on a beatmap with no storyboard --- .../Backgrounds/BeatmapBackgroundWithStoryboard.cs | 8 +++----- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index e78a93396e..75cebb275f 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Threading; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -74,15 +73,14 @@ namespace osu.Game.Graphics.Backgrounds public void UnloadStoryboard(Action scheduleStoryboardRemoval) { - Debug.Assert(drawableStoryboard != null); + if (drawableStoryboard == null) + return; loadCancellationSource.AsNonNull().Cancel(); loadCancellationSource = null; - DrawableStoryboard s = drawableStoryboard; - storyboardContainer.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); - scheduleStoryboardRemoval(s); + scheduleStoryboardRemoval(drawableStoryboard); drawableStoryboard = null; } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 66835363b4..e22f61d806 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Threading; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; From cc9aeb53079f6f00af307fb0544e0335d9eebcc8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 9 Nov 2023 22:57:12 +0300 Subject: [PATCH 621/896] Add test coverage --- .../TestSceneBackgroundScreenDefault.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs index e902303505..37f2ee0b3f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs @@ -205,6 +205,30 @@ namespace osu.Game.Tests.Visual.Background AddUntilStep("storyboard reloaded", () => screen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); } + [Test] + public void TestBeatmapBackgroundWithStoryboardButBeatmapHasNone() + { + BackgroundScreenBeatmap nestedScreen = null; + + setSupporter(true); + setSourceMode(BackgroundSource.BeatmapWithStoryboard); + + AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground()); + AddAssert("background changed", () => screen.CheckLastLoadChange() == true); + AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackgroundWithStoryboard)); + + AddUntilStep("no storyboard loaded", () => !screen.ChildrenOfType().Any()); + + AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value))); + AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen()); + + AddUntilStep("still no storyboard", () => !screen.ChildrenOfType().Any()); + + AddStep("go back", () => screen.MakeCurrent()); + + AddUntilStep("still no storyboard", () => !screen.ChildrenOfType().Any()); + } + [Test] public void TestBackgroundTypeSwitch() { From e9471589697083d8ea2214b299aca226085e7de9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 9 Nov 2023 23:03:01 +0300 Subject: [PATCH 622/896] Remove fade out transition Unnecessary addition from this PR, makes the background fade to ugly black during transition between screens. --- osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 75cebb275f..9a3d64549b 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -79,7 +79,6 @@ namespace osu.Game.Graphics.Backgrounds loadCancellationSource.AsNonNull().Cancel(); loadCancellationSource = null; - storyboardContainer.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); scheduleStoryboardRemoval(drawableStoryboard); drawableStoryboard = null; From 59998b507acf084cc8d4ffbe85e16d1f3fab9229 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 9 Nov 2023 23:23:49 +0300 Subject: [PATCH 623/896] Hide background sprite when storyboard finishes loading --- .../BeatmapBackgroundWithStoryboard.cs | 16 +++++++++++----- osu.Game/Screens/BackgroundScreenStack.cs | 4 ++-- .../Backgrounds/BackgroundScreenDefault.cs | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 9a3d64549b..1e702967b6 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -58,20 +58,20 @@ namespace osu.Game.Graphics.Backgrounds if (!Beatmap.Storyboard.HasDrawable) return; - if (Beatmap.Storyboard.ReplacesBackground) - Sprite.Alpha = 0; - LoadComponentAsync(drawableStoryboard = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock }, s => { + if (Beatmap.Storyboard.ReplacesBackground) + Sprite.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.InQuint); + storyboardContainer.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); storyboardContainer.Add(s); }, (loadCancellationSource = new CancellationTokenSource()).Token); } - public void UnloadStoryboard(Action scheduleStoryboardRemoval) + public void UnloadStoryboard(Action scheduleStoryboardRemoval) { if (drawableStoryboard == null) return; @@ -79,7 +79,13 @@ namespace osu.Game.Graphics.Backgrounds loadCancellationSource.AsNonNull().Cancel(); loadCancellationSource = null; - scheduleStoryboardRemoval(drawableStoryboard); + DrawableStoryboard s = drawableStoryboard; + + scheduleStoryboardRemoval(() => + { + s.RemoveAndDisposeImmediately(); + Sprite.Alpha = 1f; + }); drawableStoryboard = null; } diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index 3ec1835669..9af6601aa4 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.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 System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Screens; -using osu.Game.Storyboards.Drawables; namespace osu.Game.Screens { @@ -35,6 +35,6 @@ namespace osu.Game.Screens return true; } - public void ScheduleStoryboardDisposal(DrawableStoryboard storyboard) => Scheduler.AddDelayed(storyboard.RemoveAndDisposeImmediately, BackgroundScreen.TRANSITION_LENGTH); + internal void ScheduleToTransitionEnd(Action action) => Scheduler.AddDelayed(action, BackgroundScreen.TRANSITION_LENGTH); } } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index e22f61d806..07b1cc6df4 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -79,7 +79,7 @@ namespace osu.Game.Screens.Backgrounds Debug.Assert(backgroundScreenStack != null); if (background is BeatmapBackgroundWithStoryboard storyboardBackground) - storyboardBackground.UnloadStoryboard(backgroundScreenStack.ScheduleStoryboardDisposal); + storyboardBackground.UnloadStoryboard(backgroundScreenStack.ScheduleToTransitionEnd); base.OnSuspending(e); } From 6f5d905ce766da9b2e3753585a41298118e9c6b9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 10 Nov 2023 00:34:29 +0300 Subject: [PATCH 624/896] Update argon accuracy counter design --- .../Screens/Play/HUD/ArgonAccuracyCounter.cs | 59 +++++++++++- .../Play/HUD/ArgonCounterTextComponent.cs | 94 ++++++++++--------- .../Screens/Play/HUD/ArgonScoreCounter.cs | 5 - 3 files changed, 107 insertions(+), 51 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index 0414cbaea4..160841b40f 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -3,7 +3,9 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Skinning; using osuTK; @@ -22,9 +24,64 @@ namespace osu.Game.Screens.Play.HUD public bool UsesFixedAnchor { get; set; } - protected override IHasText CreateText() => new ArgonCounterTextComponent(Anchor.TopLeft, "ACCURACY", new Vector2(-4, 0)) + protected override IHasText CreateText() => new ArgonAccuracyTextComponent { WireframeOpacity = { BindTarget = WireframeOpacity }, }; + + private partial class ArgonAccuracyTextComponent : CompositeDrawable, IHasText + { + private readonly ArgonCounterTextComponent wholePart; + private readonly ArgonCounterTextComponent fractionPart; + + public IBindable WireframeOpacity { get; } = new BindableFloat(); + + public LocalisableString Text + { + get => wholePart.Text; + set + { + string[] split = value.ToString().Replace("%", string.Empty).Split("."); + + wholePart.Text = split[0]; + fractionPart.Text = "." + split[1]; + } + } + + public ArgonAccuracyTextComponent() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + Child = wholePart = new ArgonCounterTextComponent(Anchor.TopRight, "ACCURACY") + { + RequiredDisplayDigits = { Value = 3 }, + WireframeOpacity = { BindTarget = WireframeOpacity } + } + }, + fractionPart = new ArgonCounterTextComponent(Anchor.TopLeft) + { + Margin = new MarginPadding { Top = 12f * 2f + 4f }, // +4 to account for the extra spaces above the digits. + WireframeOpacity = { BindTarget = WireframeOpacity }, + Scale = new Vector2(0.5f), + }, + new ArgonCounterTextComponent(Anchor.TopLeft) + { + Text = @"%", + Margin = new MarginPadding { Top = 12f }, + WireframeOpacity = { BindTarget = WireframeOpacity } + }, + } + }; + } + } } } diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 3d8546e0e3..759a5dbfea 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,62 +20,30 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonCounterTextComponent : CompositeDrawable, IHasText { - private readonly LocalisableString? label; - private readonly ArgonCounterSpriteText wireframesPart; private readonly ArgonCounterSpriteText textPart; + private readonly OsuSpriteText labelText; public IBindable WireframeOpacity { get; } = new BindableFloat(); + public Bindable RequiredDisplayDigits { get; } = new BindableInt(); public LocalisableString Text { get => textPart.Text; set { - wireframesPart.Text = FormatWireframes(value); + int remainingCount = RequiredDisplayDigits.Value - value.ToString().Count(char.IsDigit); + string remainingText = remainingCount > 0 ? new string('#', remainingCount) : string.Empty; + + wireframesPart.Text = remainingText + value; textPart.Text = value; } } - public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null, Vector2? spacing = null) + public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null) { Anchor = anchor; Origin = anchor; - - this.label = label; - - wireframesPart = new ArgonCounterSpriteText(c => - { - if (c == '.') - return @"dot"; - - return @"wireframes"; - }) - { - Anchor = anchor, - Origin = anchor, - Spacing = spacing ?? new Vector2(-2, 0), - }; - textPart = new ArgonCounterSpriteText(c => - { - if (c == '.') - return @"dot"; - - if (c == '%') - return @"percentage"; - - return c.ToString(); - }) - { - Anchor = anchor, - Origin = anchor, - Spacing = spacing ?? new Vector2(-2, 0), - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { AutoSizeAxes = Axes.Both; InternalChild = new FillFlowContainer @@ -83,12 +52,11 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + labelText = new OsuSpriteText { Alpha = label != null ? 1 : 0, Text = label.GetValueOrDefault(), Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), - Colour = colours.Blue0, Margin = new MarginPadding { Left = 2.5f }, }, new Container @@ -96,14 +64,50 @@ namespace osu.Game.Screens.Play.HUD AutoSizeAxes = Axes.Both, Children = new[] { - wireframesPart, - textPart, + wireframesPart = new ArgonCounterSpriteText(wireframesLookup) + { + Anchor = anchor, + Origin = anchor, + }, + textPart = new ArgonCounterSpriteText(textLookup) + { + Anchor = anchor, + Origin = anchor, + }, } } } }; } + private string textLookup(char c) + { + switch (c) + { + case '.': + return @"dot"; + + case '%': + return @"percentage"; + + default: + return c.ToString(); + } + } + + private string wireframesLookup(char c) + { + if (c == '.') return @"dot"; + + return @"wireframes"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + labelText.Colour = colours.Blue0; + } + protected virtual LocalisableString FormatWireframes(LocalisableString text) => text; protected override void LoadComplete() @@ -131,8 +135,8 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(ISkinSource skin) { - // todo: rename font - Font = new FontUsage(@"argon-score", 1); + Spacing = new Vector2(-2f, 0f); + Font = new FontUsage(@"argon-counter", 1); glyphStore = new GlyphStore(skin, getLookup); } diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index fef4199d31..b15c21801f 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.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.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -34,14 +33,10 @@ namespace osu.Game.Screens.Play.HUD private partial class ArgonScoreTextComponent : ArgonCounterTextComponent { - public IBindable RequiredDisplayDigits { get; } = new BindableInt(); - public ArgonScoreTextComponent(Anchor anchor, LocalisableString? label = null) : base(anchor, label) { } - - protected override LocalisableString FormatWireframes(LocalisableString text) => new string('#', Math.Max(text.ToString().Length, RequiredDisplayDigits.Value)); } } } From 7fc2050f7227424b7e734260126146c48e99358e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 11:52:34 +0900 Subject: [PATCH 625/896] Rename variables and mak seconary tooltip type work again --- .../Online/TestSceneUserClickableAvatar.cs | 9 ++- .../OnlinePlay/Components/ParticipantsList.cs | 2 +- .../DrawableRoomParticipantsList.cs | 2 +- osu.Game/Users/Drawables/ClickableAvatar.cs | 74 ++++++++++++++----- osu.Game/Users/Drawables/UpdateableAvatar.cs | 11 ++- 5 files changed, 69 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 50e5653ad5..93e2583eb7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -102,9 +102,9 @@ namespace osu.Game.Tests.Visual.Online AddWaitStep("wait for tooltip to hide", 3); } - private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool onlyUsername, string? color = null) + private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool showPanel, string? color = null) { - return new ClickableAvatar(new APIUser + var user = new APIUser { Username = username, Id = id, @@ -115,7 +115,9 @@ namespace osu.Game.Tests.Visual.Online { Value = new UserStatusOnline() }, - }) + }; + + return new ClickableAvatar(user, showPanel) { Width = 50, Height = 50, @@ -125,7 +127,6 @@ namespace osu.Game.Tests.Visual.Online { Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), }, - ShowUsernameOnly = onlyUsername, }; } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index 8cde7859b2..c4aefe4f99 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Components RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex(@"27252d"), }, - avatar = new UpdateableAvatar(showUsernameOnly: true) { RelativeSizeAxes = Axes.Both }, + avatar = new UpdateableAvatar(showUserPanelOnHover: true) { RelativeSizeAxes = Axes.Both }, }; } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 65f0555612..60e05285d9 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -289,7 +289,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components set => avatar.User = value; } - private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameOnly: true) { RelativeSizeAxes = Axes.Both }; + private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUserPanelOnHover: true) { RelativeSizeAxes = Axes.Both }; [BackgroundDependencyLoader] private void load(OverlayColourProvider colours) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 48a12acb5e..6390acc608 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -1,12 +1,15 @@ // 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.Cursor; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osu.Game.Localisation; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -15,15 +18,13 @@ namespace osu.Game.Users.Drawables { public partial class ClickableAvatar : OsuClickableContainer, IHasCustomTooltip { - // public ITooltip GetCustomTooltip() => new APIUserTooltip(user!) { ShowTooltip = TooltipEnabled }; - public ITooltip GetCustomTooltip() => new UserCardTooltip(); + public ITooltip GetCustomTooltip() => showCardOnHover ? new UserCardTooltip() : new NoCardTooltip(); public APIUser? TooltipContent { get; } private readonly APIUser? user; - // TODO: reimplement. - public bool ShowUsernameOnly { get; set; } + private readonly bool showCardOnHover; [Resolved] private OsuGame? game { get; set; } @@ -32,12 +33,15 @@ namespace osu.Game.Users.Drawables /// A clickable avatar for the specified user, with UI sounds included. /// /// The user. A null value will get a placeholder avatar. - public ClickableAvatar(APIUser? user = null) + /// + public ClickableAvatar(APIUser? user = null, bool showCardOnHover = false) { if (user?.Id != APIUser.SYSTEM_USER_ID) Action = openProfile; - TooltipContent = this.user = user; + this.showCardOnHover = showCardOnHover; + + TooltipContent = this.user = user ?? new GuestUser(); } [BackgroundDependencyLoader] @@ -65,23 +69,59 @@ namespace osu.Game.Users.Drawables public UserCardTooltip() { AutoSizeAxes = Axes.Both; - Masking = true; - CornerRadius = 5; } - protected override void PopIn() - { - this.FadeIn(20, Easing.OutQuint); - } - - protected override void PopOut() => this.FadeOut(80, Easing.OutQuint); + protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); + protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint); public void Move(Vector2 pos) => Position = pos; - public void SetContent(APIUser? content) => LoadComponentAsync(new UserGridPanel(content ?? new GuestUser()) + private APIUser? user; + + public void SetContent(APIUser? content) { - Width = 300, - }, panel => Child = panel); + if (content == user && Children.Any()) + return; + + user = content; + + if (user != null) + { + LoadComponentAsync(new UserGridPanel(user) + { + Width = 300, + }, panel => Child = panel); + } + else + { + var tooltip = new OsuTooltipContainer.OsuTooltip(); + tooltip.SetContent(ContextMenuStrings.ViewProfile); + tooltip.Show(); + + Child = tooltip; + } + } + } + + public partial class NoCardTooltip : VisibilityContainer, ITooltip + { + public NoCardTooltip() + { + var tooltip = new OsuTooltipContainer.OsuTooltip(); + tooltip.SetContent(ContextMenuStrings.ViewProfile); + tooltip.Show(); + + Child = tooltip; + } + + protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); + protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint); + + public void Move(Vector2 pos) => Position = pos; + + public void SetContent(APIUser? content) + { + } } } } diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index f220ee5a25..b020e7fa63 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -47,20 +47,20 @@ namespace osu.Game.Users.Drawables private readonly bool isInteractive; private readonly bool showGuestOnNull; - private readonly bool showUsernameOnly; + private readonly bool showUserPanelOnHover; /// /// Construct a new UpdateableAvatar. /// /// The initial user to display. /// If set to true, hover/click sounds will play and clicking the avatar will open the user's profile. - /// If set to true, the user status panel will be displayed in the tooltip. + /// If set to true, the user status panel will be displayed in the tooltip. /// Whether to show a default guest representation on null user (as opposed to nothing). - public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUsernameOnly = false, bool showGuestOnNull = true) + public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUserPanelOnHover = false, bool showGuestOnNull = true) { this.isInteractive = isInteractive; this.showGuestOnNull = showGuestOnNull; - this.showUsernameOnly = showUsernameOnly; + this.showUserPanelOnHover = showUserPanelOnHover; User = user; } @@ -72,10 +72,9 @@ namespace osu.Game.Users.Drawables if (isInteractive) { - return new ClickableAvatar(user) + return new ClickableAvatar(user, showUserPanelOnHover) { RelativeSizeAxes = Axes.Both, - ShowUsernameOnly = showUsernameOnly }; } From e0a5ec5352d0edf5d73513e203cdeda51624a302 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 12:09:16 +0900 Subject: [PATCH 626/896] Revert incorrect classic mod test assertions --- .../OsuDifficultyCalculatorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 1d76c24620..fa7454b435 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -23,14 +23,14 @@ namespace osu.Game.Rulesets.Osu.Tests => base.Test(expectedStarRating, expectedMaxCombo, name); [TestCase(8.9742952703071666d, 239, "diffcalc-test")] - [TestCase(0.55071082800473514d, 4, "very-fast-slider")] [TestCase(1.743180218215227d, 54, "zero-length-sliders")] + [TestCase(0.55071082800473514d, 4, "very-fast-slider")] public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); - [TestCase(6.710442985146793d, 272, "diffcalc-test")] - [TestCase(0.42506480230838789d, 6, "very-fast-slider")] - [TestCase(1.4386882251130073d, 63, "zero-length-sliders")] + [TestCase(6.710442985146793d, 239, "diffcalc-test")] + [TestCase(1.4386882251130073d, 54, "zero-length-sliders")] + [TestCase(0.42506480230838789d, 4, "very-fast-slider")] public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); From fb02c317507b7c58770bdd52984eb6a0f91f9b53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 12:09:48 +0900 Subject: [PATCH 627/896] Fix typo in assert step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 1e295aae70..c9d721d1c4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -468,7 +468,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void assertHeadMissTailTracked() { AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit)); - AddAssert("Slider head misseed", () => judgementResults.First().IsHit, () => Is.False); + AddAssert("Slider head missed", () => judgementResults.First().IsHit, () => Is.False); } private void assertMidSliderJudgements() From f13648418b82f7158a733588e8caf4f094efc1ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 12:10:52 +0900 Subject: [PATCH 628/896] Update `HitResultTest` to be more conformant to original expectations --- osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs index 72acd18c5b..e003c9c534 100644 --- a/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/HitResultTest.cs @@ -11,14 +11,14 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestFixture] public class HitResultTest { - [TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })] - [TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })] - [TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })] + [TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss, HitResult.IgnoreMiss })] + [TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss, HitResult.IgnoreMiss })] + [TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss, HitResult.IgnoreMiss })] [TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })] [TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })] public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults) { - HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => t != HitResult.IgnoreMiss && !minResults.Contains(t)).ToArray(); + HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => !minResults.Contains(t)).ToArray(); Assert.Multiple(() => { From 44c0442f4fc7f29f94ea1acad609ab8a676788d0 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Nov 2023 14:00:34 +0900 Subject: [PATCH 629/896] Adjust comment on Slider's judgement --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index cb6827b428..506145568e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -276,9 +276,9 @@ namespace osu.Game.Rulesets.Osu.Objects } public override Judgement CreateJudgement() => ClassicSliderBehaviour - // See logic in `DrawableSlider.CheckForResult()` + // Final combo is provided by the slider itself - see logic in `DrawableSlider.CheckForResult()` ? new OsuJudgement() - // Of note, this creates a combo discrepancy for non-classic-mod sliders (there is no combo increase for tail or slider judgement). + // Final combo is provided by the tail circle - see `SliderTailCircle` : new OsuIgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; From b0c5b3cb1097861506b11862b996b91afe7a384a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 14:15:42 +0900 Subject: [PATCH 630/896] Add `Size` to serialised components of a `SerialisedDrawableInfo` --- osu.Game/Skinning/SerialisableDrawableExtensions.cs | 3 +++ osu.Game/Skinning/SerialisedDrawableInfo.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game/Skinning/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs index 51b57a000d..0b44db9bdc 100644 --- a/osu.Game/Skinning/SerialisableDrawableExtensions.cs +++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Extensions; +using osuTK; namespace osu.Game.Skinning { @@ -18,6 +19,8 @@ namespace osu.Game.Skinning // todo: can probably make this better via deserialisation directly using a common interface. component.Position = drawableInfo.Position; component.Rotation = drawableInfo.Rotation; + if (drawableInfo.Size != Vector2.Zero && (component as CompositeDrawable)?.AutoSizeAxes == Axes.None) + component.Size = drawableInfo.Size; component.Scale = drawableInfo.Scale; component.Anchor = drawableInfo.Anchor; component.Origin = drawableInfo.Origin; diff --git a/osu.Game/Skinning/SerialisedDrawableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs index c515f228f7..0705e91d6d 100644 --- a/osu.Game/Skinning/SerialisedDrawableInfo.cs +++ b/osu.Game/Skinning/SerialisedDrawableInfo.cs @@ -35,6 +35,8 @@ namespace osu.Game.Skinning public Vector2 Scale { get; set; } + public Vector2 Size { get; set; } + public Anchor Anchor { get; set; } public Anchor Origin { get; set; } @@ -62,6 +64,7 @@ namespace osu.Game.Skinning Position = component.Position; Rotation = component.Rotation; Scale = component.Scale; + Size = component.Size; Anchor = component.Anchor; Origin = component.Origin; From ec3b6e47fb7c6b302e633132362bfe54873a58a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 14:17:11 +0900 Subject: [PATCH 631/896] Change selection handling to adjust `Size` instead of `Scale` for edge nodes --- .../Edit/OsuSelectionHandler.cs | 1 + .../SkinEditor/SkinSelectionHandler.cs | 40 +++++++++++++++++-- .../Edit/Compose/Components/SelectionBox.cs | 19 ++++++++- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index e81941d254..4da640a971 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; + SelectionBox.CanScaleProportionally = SelectionBox.CanScaleX && SelectionBox.CanScaleY; SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); } diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index df73b15101..bf03906e56 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; @@ -31,8 +32,38 @@ namespace osu.Game.Overlays.SkinEditor UpdatePosition = updateDrawablePosition }; + private bool allSelectedSupportManualSizing => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes == Axes.None); + public override bool HandleScale(Vector2 scale, Anchor anchor) { + bool adjustSize; + + switch (anchor) + { + // for corners, adjust scale. + case Anchor.TopLeft: + case Anchor.TopRight: + case Anchor.BottomLeft: + case Anchor.BottomRight: + adjustSize = false; + break; + + // for edges, adjust size. + case Anchor.TopCentre: + case Anchor.CentreLeft: + case Anchor.CentreRight: + case Anchor.BottomCentre: + // autosize elements can't be easily handled so just disable sizing for now. + if (!allSelectedSupportManualSizing) + return false; + + adjustSize = true; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(anchor), anchor, null); + } + // convert scale to screen space scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero); @@ -120,7 +151,10 @@ namespace osu.Game.Overlays.SkinEditor if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90)) currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X); - drawableItem.Scale *= currentScaledDelta; + if (adjustSize) + drawableItem.Size *= currentScaledDelta; + else + drawableItem.Scale *= currentScaledDelta; } return true; @@ -169,8 +203,8 @@ namespace osu.Game.Overlays.SkinEditor { base.OnSelectionChanged(); - SelectionBox.CanScaleX = true; - SelectionBox.CanScaleY = true; + SelectionBox.CanScaleX = SelectionBox.CanScaleY = allSelectedSupportManualSizing; + SelectionBox.CanScaleProportionally = true; SelectionBox.CanFlipX = true; SelectionBox.CanFlipY = true; SelectionBox.CanReverse = false; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 72d96213ee..0917867a61 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -91,6 +91,23 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private bool canScaleProportionally; + + /// + /// Whether vertical scaling support should be enabled. + /// + public bool CanScaleProportionally + { + get => canScaleProportionally; + set + { + if (canScaleProportionally == value) return; + + canScaleProportionally = value; + recreate(); + } + } + private bool canFlipX; /// @@ -245,7 +262,7 @@ namespace osu.Game.Screens.Edit.Compose.Components }; if (CanScaleX) addXScaleComponents(); - if (CanScaleX && CanScaleY) addFullScaleComponents(); + if (CanScaleProportionally) addFullScaleComponents(); if (CanScaleY) addYScaleComponents(); if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); From fb361a4e0a8152bb35fe16cdc98bf06993f64a67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 14:25:55 +0900 Subject: [PATCH 632/896] Reset size along with scale for relative items --- osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index bf03906e56..dab8c1c558 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -249,7 +249,12 @@ namespace osu.Game.Overlays.SkinEditor yield return new OsuMenuItem("Reset scale", MenuItemType.Standard, () => { foreach (var blueprint in SelectedBlueprints) - ((Drawable)blueprint.Item).Scale = Vector2.One; + { + var blueprintItem = ((Drawable)blueprint.Item); + blueprintItem.Scale = Vector2.One; + if (RelativeSizeAxes == Axes.Both) + blueprintItem.Size = Vector2.One; + } }); yield return new EditorMenuItemSpacer(); From 0add035c674d59bd36ef129d474a76550e3ced92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 14:31:02 +0900 Subject: [PATCH 633/896] Disable resizing of `LegacySongProgress` Because it looks bad. --- osu.Game/Skinning/LegacySongProgress.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacySongProgress.cs b/osu.Game/Skinning/LegacySongProgress.cs index 22aea99291..26004ff111 100644 --- a/osu.Game/Skinning/LegacySongProgress.cs +++ b/osu.Game/Skinning/LegacySongProgress.cs @@ -19,11 +19,14 @@ namespace osu.Game.Skinning public override bool HandleNonPositionalInput => false; public override bool HandlePositionalInput => false; + public LegacySongProgress() + { + AutoSizeAxes = Axes.Both; + } + [BackgroundDependencyLoader] private void load() { - Size = new Vector2(33); - InternalChildren = new Drawable[] { new Container @@ -39,7 +42,7 @@ namespace osu.Game.Skinning }, new CircularContainer { - RelativeSizeAxes = Axes.Both, + Size = new Vector2(33), Masking = true, BorderColour = Colour4.White, BorderThickness = 2, From 175dae49c623bfa8632608e204cac168d0b9723a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 14:43:51 +0900 Subject: [PATCH 634/896] Reset scale per axis --- osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index dab8c1c558..77e522a925 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -252,8 +252,11 @@ namespace osu.Game.Overlays.SkinEditor { var blueprintItem = ((Drawable)blueprint.Item); blueprintItem.Scale = Vector2.One; - if (RelativeSizeAxes == Axes.Both) - blueprintItem.Size = Vector2.One; + + if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.X)) + blueprintItem.Width = 1; + if (blueprintItem.RelativeSizeAxes.HasFlagFast(Axes.Y)) + blueprintItem.Height = 1; } }); From 93ff82bc80c8bc06bc78149fd49584396ca982bd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Nov 2023 14:52:03 +0900 Subject: [PATCH 635/896] Attempt to support quotes in handling of GH comment body --- .github/workflows/diffcalc.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index e7c628e365..d4150208d3 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -185,9 +185,11 @@ jobs: - name: Add comment environment if: ${{ github.event_name == 'issue_comment' }} + env: + COMMENT_BODY: ${{ github.event.comment.body }} run: | # Add comment environment - echo '${{ github.event.comment.body }}' | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do + echo $COMMENT_BODY | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do opt=$(echo ${line} | cut -d '=' -f1) sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}" done From f31c1c9c7941db573a9516710e42e53946947e25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 12:58:07 +0900 Subject: [PATCH 636/896] Rename and move skinnable line component to a more commomn place --- osu.Game/Skinning/ArgonSkin.cs | 5 +++-- .../Components/RoundedLine.cs} | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/{Screens/Play/HUD/ArgonHealthRightLine.cs => Skinning/Components/RoundedLine.cs} (73%) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index d598c04891..b3eff41495 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -16,6 +16,7 @@ using osu.Game.IO; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osu.Game.Skinning.Components; using osuTK; using osuTK.Graphics; @@ -115,7 +116,7 @@ namespace osu.Game.Skinning var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { var health = container.OfType().FirstOrDefault(); - var healthLine = container.OfType().FirstOrDefault(); + var healthLine = container.OfType().FirstOrDefault(); var scoreWedge = container.OfType().FirstOrDefault(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); @@ -207,7 +208,7 @@ namespace osu.Game.Skinning new ArgonScoreWedge(), new ArgonScoreCounter(), new ArgonHealthDisplay(), - new ArgonHealthRightLine(), + new RoundedLine(), new ArgonAccuracyCounter(), new ArgonComboCounter(), new BarHitErrorMeter(), diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthRightLine.cs b/osu.Game/Skinning/Components/RoundedLine.cs similarity index 73% rename from osu.Game/Screens/Play/HUD/ArgonHealthRightLine.cs rename to osu.Game/Skinning/Components/RoundedLine.cs index 25918f679c..d7b12c3f4c 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthRightLine.cs +++ b/osu.Game/Skinning/Components/RoundedLine.cs @@ -5,19 +5,18 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Skinning; -using osuTK; -namespace osu.Game.Screens.Play.HUD +namespace osu.Game.Skinning.Components { - public partial class ArgonHealthRightLine : CompositeDrawable, ISerialisableDrawable + public partial class RoundedLine : CompositeDrawable, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } [BackgroundDependencyLoader] private void load() { - Size = new Vector2(50f, ArgonHealthDisplay.MAIN_PATH_RADIUS * 2); + AutoSizeAxes = Axes.Both; + InternalChild = new Circle { Anchor = Anchor.CentreLeft, From 99d9db5b76ba5292398201039e75fcb0a9bcc1d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 14:03:24 +0900 Subject: [PATCH 637/896] Use a better default size for line --- osu.Game/Skinning/ArgonSkin.cs | 7 ++++++- osu.Game/Skinning/Components/RoundedLine.cs | 19 ++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index b3eff41495..03b089582d 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -137,7 +137,12 @@ namespace osu.Game.Skinning health.Position = new Vector2(components_x_offset, 20f); if (healthLine != null) - healthLine.Y = health.Y; + { + healthLine.Anchor = Anchor.TopLeft; + healthLine.Origin = Anchor.CentreLeft; + healthLine.Y = health.Y + ArgonHealthDisplay.MAIN_PATH_RADIUS; + healthLine.Size = new Vector2(45, 3); + } if (scoreWedge != null) { diff --git a/osu.Game/Skinning/Components/RoundedLine.cs b/osu.Game/Skinning/Components/RoundedLine.cs index d7b12c3f4c..491f87d31e 100644 --- a/osu.Game/Skinning/Components/RoundedLine.cs +++ b/osu.Game/Skinning/Components/RoundedLine.cs @@ -1,29 +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.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osuTK; namespace osu.Game.Skinning.Components { - public partial class RoundedLine : CompositeDrawable, ISerialisableDrawable + public partial class RoundedLine : Circle, ISerialisableDrawable { public bool UsesFixedAnchor { get; set; } - [BackgroundDependencyLoader] - private void load() + public RoundedLine() { - AutoSizeAxes = Axes.Both; - - InternalChild = new Circle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = 45f, - Height = 3f, - }; + Size = new Vector2(200, 8); } } } From 6c1d48dfaf0e4f06489b22c32b437a2d18d5250a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 14:51:14 +0900 Subject: [PATCH 638/896] Remove unused function --- osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 759a5dbfea..dbeafe5b59 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -108,8 +108,6 @@ namespace osu.Game.Screens.Play.HUD labelText.Colour = colours.Blue0; } - protected virtual LocalisableString FormatWireframes(LocalisableString text) => text; - protected override void LoadComplete() { base.LoadComplete(); From 7c3a626f4b2a1706cfc46295770b8673b374aee4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 15:06:07 +0900 Subject: [PATCH 639/896] Add basic animation for combo counter --- osu.Game/Screens/Play/HUD/ArgonComboCounter.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index 6a7d5ff665..36194d787b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -8,11 +8,15 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Screens.Play.HUD { public partial class ArgonComboCounter : ComboCounter { + protected override double RollingDuration => 500; + protected override Easing RollingEasing => Easing.OutQuint; + [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) { @@ -25,6 +29,12 @@ namespace osu.Game.Screens.Play.HUD private void load(ScoreProcessor scoreProcessor) { Current.BindTo(scoreProcessor.Combo); + Current.BindValueChanged(combo => + { + bool wasIncrease = combo.NewValue > combo.OldValue; + DrawableCount.ScaleTo(new Vector2(1, wasIncrease ? 1.2f : 0.8f)) + .ScaleTo(Vector2.One, 600, Easing.OutQuint); + }); } protected override LocalisableString FormatCount(int count) => $@"{count}x"; From e861681cd4a081b3cc557df725cb0d2737096ebc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 15:06:19 +0900 Subject: [PATCH 640/896] Adjust argon rolling easings --- osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs | 3 +++ osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index 160841b40f..ee5e16180e 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -14,6 +14,9 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable { + protected override double RollingDuration => 500; + protected override Easing RollingEasing => Easing.OutQuint; + [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) { diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index b15c21801f..5ec359f5bb 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -13,6 +13,9 @@ namespace osu.Game.Screens.Play.HUD { public partial class ArgonScoreCounter : GameplayScoreCounter, ISerialisableDrawable { + protected override double RollingDuration => 500; + protected override Easing RollingEasing => Easing.OutQuint; + [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) { From 7e0b41219cd916931bd18d32f87705624b4e723b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 15:26:37 +0900 Subject: [PATCH 641/896] Change animation to only affect number portion, add miss animation --- .../Screens/Play/HUD/ArgonComboCounter.cs | 20 ++++++++++++++++--- .../Play/HUD/ArgonCounterTextComponent.cs | 4 +++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index 36194d787b..b75d4268fc 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.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.Bindables; using osu.Framework.Graphics; @@ -9,11 +10,14 @@ using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { public partial class ArgonComboCounter : ComboCounter { + private ArgonCounterTextComponent text = null!; + protected override double RollingDuration => 500; protected override Easing RollingEasing => Easing.OutQuint; @@ -32,14 +36,24 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(combo => { bool wasIncrease = combo.NewValue > combo.OldValue; - DrawableCount.ScaleTo(new Vector2(1, wasIncrease ? 1.2f : 0.8f)) - .ScaleTo(Vector2.One, 600, Easing.OutQuint); + bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0; + + float newScale = Math.Clamp(text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f); + + float duration = wasMiss ? 2000 : 300; + + text.NumberContainer + .ScaleTo(new Vector2(newScale)) + .ScaleTo(Vector2.One, duration, Easing.OutQuint); + + if (wasMiss) + text.FlashColour(Color4.Red, duration, Easing.OutQuint); }); } protected override LocalisableString FormatCount(int count) => $@"{count}x"; - protected override IHasText CreateText() => new ArgonCounterTextComponent(Anchor.TopLeft, "COMBO") + protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, "COMBO") { WireframeOpacity = { BindTarget = WireframeOpacity }, }; diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index dbeafe5b59..56f60deae1 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -27,6 +27,8 @@ namespace osu.Game.Screens.Play.HUD public IBindable WireframeOpacity { get; } = new BindableFloat(); public Bindable RequiredDisplayDigits { get; } = new BindableInt(); + public Container NumberContainer { get; private set; } + public LocalisableString Text { get => textPart.Text; @@ -59,7 +61,7 @@ namespace osu.Game.Screens.Play.HUD Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), Margin = new MarginPadding { Left = 2.5f }, }, - new Container + NumberContainer = new Container { AutoSizeAxes = Axes.Both, Children = new[] From 4f90ac15fad7df54047abd01b6f88b6e28310277 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 15:39:17 +0900 Subject: [PATCH 642/896] Reduce the default wireframe opacity a bit --- osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs | 2 +- osu.Game/Screens/Play/HUD/ArgonComboCounter.cs | 2 +- osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index ee5e16180e..5f9441a5c4 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.HUD protected override Easing RollingEasing => Easing.OutQuint; [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] - public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) { Precision = 0.01f, MinValue = 0, diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index b75d4268fc..63a17529f5 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.HUD protected override Easing RollingEasing => Easing.OutQuint; [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] - public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) { Precision = 0.01f, MinValue = 0, diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 5ec359f5bb..0192fa3c02 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.HUD protected override Easing RollingEasing => Easing.OutQuint; [SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")] - public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.4f) + public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f) { Precision = 0.01f, MinValue = 0, From 1818a84034c57f76335e06371b6a68296b0126f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 15:52:02 +0900 Subject: [PATCH 643/896] Rewrite tests to not rely on realtime clock Definitely faster, hopefully more reliable too. --- .../Mods/TestSceneOsuModTouchDevice.cs | 150 +++++++++++------- 1 file changed, 94 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index abeb56209e..f528795125 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -5,6 +5,8 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Input; +using osu.Framework.Screens; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; using osu.Game.Configuration; @@ -14,39 +16,24 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; +using osu.Game.Storyboards; using osu.Game.Tests.Visual; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public partial class TestSceneOsuModTouchDevice : PlayerTestScene + public partial class TestSceneOsuModTouchDevice : RateAdjustedBeatmapTestScene { [Resolved] private SessionStatics statics { get; set; } = null!; - protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + private ScoreAccessibleSoloPlayer currentPlayer = null!; + private readonly ManualClock manualClock = new ManualClock { Rate = 0 }; - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => - new OsuBeatmap - { - HitObjects = - { - new HitCircle - { - Position = OsuPlayfield.BASE_SIZE / 2, - StartTime = 0, - }, - new HitCircle - { - Position = OsuPlayfield.BASE_SIZE / 2, - StartTime = 5000, - }, - }, - Breaks = - { - new BreakPeriod(2000, 3000) - } - }; + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio); [BackgroundDependencyLoader] private void load() @@ -63,103 +50,154 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [Test] public void TestUserAlreadyHasTouchDeviceActive() { + loadPlayer(); // it is presumed that a previous screen (i.e. song select) will set this up AddStep("set up touchscreen user", () => { - Player.Score.ScoreInfo.Mods = Player.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray(); + currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray(); statics.SetValue(Static.TouchInputActive, true); }); - AddUntilStep("wait until 0 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0).Within(500)); - AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); - AddUntilStep("wait until 0", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + + AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0)); + AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); AddStep("touch circle", () => { - var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); - AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); } [Test] public void TestTouchDuringBreak() { - AddUntilStep("wait until 2000 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000).Within(500)); - AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); - AddUntilStep("wait until 2000", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000)); + loadPlayer(); + AddStep("seek to 2000", () => currentPlayer.GameplayClockContainer.Seek(2000)); + AddUntilStep("wait until 2000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000)); AddStep("touch playfield", () => { - var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); - AddAssert("touch device mod not activated", () => Player.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); + AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); } [Test] public void TestTouchMiss() { + loadPlayer(); // ensure mouse is active (and that it's not suppressed due to touches in previous tests) AddStep("click mouse", () => InputManager.Click(MouseButton.Left)); - AddUntilStep("wait until 200 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200).Within(500)); - AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); - AddUntilStep("wait until 200", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200)); + AddStep("seek to 200", () => currentPlayer.GameplayClockContainer.Seek(200)); + AddUntilStep("wait until 200", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200)); AddStep("touch playfield", () => { - var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); - AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); } [Test] public void TestIncompatibleModActive() { + loadPlayer(); // this is only a veneer of enabling autopilot as having it actually active from the start is annoying to make happen // given the tests' structure. - AddStep("enable autopilot", () => Player.Score.ScoreInfo.Mods = new Mod[] { new OsuModAutopilot() }); + AddStep("enable autopilot", () => currentPlayer.Score.ScoreInfo.Mods = new Mod[] { new OsuModAutopilot() }); - AddUntilStep("wait until 0 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0).Within(500)); - AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); - AddUntilStep("wait until 0", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0)); + AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); AddStep("touch playfield", () => { - var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); - AddAssert("touch device mod not activated", () => Player.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); + AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); } [Test] public void TestSecondObjectTouched() { + loadPlayer(); // ensure mouse is active (and that it's not suppressed due to touches in previous tests) AddStep("click mouse", () => InputManager.Click(MouseButton.Left)); - AddUntilStep("wait until 0 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0).Within(500)); - AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); - AddUntilStep("wait until 0", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); + AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0)); + AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0)); AddStep("click circle", () => { - InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + InputManager.MoveMouseTo(currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); InputManager.Click(MouseButton.Left); }); - AddAssert("touch device mod not activated", () => Player.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); + AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf()); - AddStep("speed back up", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 1); - AddUntilStep("wait until 5000 near", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000).Within(500)); - AddStep("slow down", () => Player.GameplayClockContainer.AdjustmentsFromMods.Frequency.Value = 0.2); - AddUntilStep("wait until 5000", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000)); + AddStep("seek to 5000", () => currentPlayer.GameplayClockContainer.Seek(5000)); + AddUntilStep("wait until 5000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000)); AddStep("touch playfield", () => { - var touch = new Touch(TouchSource.Touch1, Player.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); + var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); InputManager.BeginTouch(touch); InputManager.EndTouch(touch); }); - AddAssert("touch device mod activated", () => Player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf()); + } + + private void loadPlayer() + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new OsuBeatmap + { + HitObjects = + { + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 0, + }, + new HitCircle + { + Position = OsuPlayfield.BASE_SIZE / 2, + StartTime = 5000, + }, + }, + Breaks = + { + new BreakPeriod(2000, 3000) + } + }); + + var p = new ScoreAccessibleSoloPlayer(); + + LoadScreen(currentPlayer = p); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + } + + private partial class ScoreAccessibleSoloPlayer : SoloPlayer + { + public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer; + + public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleSoloPlayer() + : base(new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } } } } From 8690b0876928570e5df1d25a1568db6ec703ee05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 16:08:45 +0900 Subject: [PATCH 644/896] Fix compose select box test failures --- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 80c69aacf6..9e8d75efea 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -47,6 +47,7 @@ namespace osu.Game.Tests.Visual.Editing CanScaleX = true, CanScaleY = true, + CanScaleProportionally = true, CanFlipX = true, CanFlipY = true, From 60df2722ab7d72e040b176d0d756493c6c2daf4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 16:04:28 +0900 Subject: [PATCH 645/896] Rename `RoundedLine` to `BoxElement` and make more generically useful --- osu.Game/Skinning/ArgonSkin.cs | 4 +- osu.Game/Skinning/Components/BoxElement.cs | 50 +++++++++++++++++++++ osu.Game/Skinning/Components/RoundedLine.cs | 18 -------- 3 files changed, 52 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Skinning/Components/BoxElement.cs delete mode 100644 osu.Game/Skinning/Components/RoundedLine.cs diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 03b089582d..b78abdffd7 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -116,7 +116,7 @@ namespace osu.Game.Skinning var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container => { var health = container.OfType().FirstOrDefault(); - var healthLine = container.OfType().FirstOrDefault(); + var healthLine = container.OfType().FirstOrDefault(); var scoreWedge = container.OfType().FirstOrDefault(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); @@ -213,7 +213,7 @@ namespace osu.Game.Skinning new ArgonScoreWedge(), new ArgonScoreCounter(), new ArgonHealthDisplay(), - new RoundedLine(), + new BoxElement(), new ArgonAccuracyCounter(), new ArgonComboCounter(), new BarHitErrorMeter(), diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs new file mode 100644 index 0000000000..235f97ceef --- /dev/null +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -0,0 +1,50 @@ +// 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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Skinning.Components +{ + public partial class BoxElement : CompositeDrawable, ISerialisableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [SettingSource("Corner rounding", "How round the corners of the box should be.")] + public BindableFloat CornerRounding { get; } = new BindableFloat(1) + { + Precision = 0.01f, + MinValue = 0, + MaxValue = 1, + }; + + public BoxElement() + { + Size = new Vector2(400, 80); + + InternalChildren = new Drawable[] + { + new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + }; + + Masking = true; + } + + protected override void Update() + { + base.Update(); + + CornerRadius = CornerRounding.Value * Math.Min(DrawWidth, DrawHeight) * 0.5f; + } + } +} diff --git a/osu.Game/Skinning/Components/RoundedLine.cs b/osu.Game/Skinning/Components/RoundedLine.cs deleted file mode 100644 index 491f87d31e..0000000000 --- a/osu.Game/Skinning/Components/RoundedLine.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 osu.Framework.Graphics.Shapes; -using osuTK; - -namespace osu.Game.Skinning.Components -{ - public partial class RoundedLine : Circle, ISerialisableDrawable - { - public bool UsesFixedAnchor { get; set; } - - public RoundedLine() - { - Size = new Vector2(200, 8); - } - } -} From 7db14baed74277383e4ac567577bb08b3b2aeea5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 16:13:45 +0900 Subject: [PATCH 646/896] 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 73cd239854..b47e2f1ca8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From b7938e78a0dc05f6847323058ef58f235e67324f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 16:30:21 +0900 Subject: [PATCH 647/896] Make sure test is in a break ...because apparently it may take a while to update the flag. --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs index f528795125..cd51ccd751 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs @@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods loadPlayer(); AddStep("seek to 2000", () => currentPlayer.GameplayClockContainer.Seek(2000)); AddUntilStep("wait until 2000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000)); + AddUntilStep("wait until break entered", () => currentPlayer.IsBreakTime.Value); AddStep("touch playfield", () => { var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre); From fa5921862f99e6ed4a9757938ad77a8bd9d066ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 16:19:42 +0900 Subject: [PATCH 648/896] Update deserialising test --- .../Archives/modified-argon-20231108.osk | Bin 1494 -> 1473 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk b/osu.Game.Tests/Resources/Archives/modified-argon-20231108.osk index d07c5171007777784216e3ee8d4bc631a602bfb9..d56c4d4dcd8cfbfbdb536cd889dc72bdb5ff76f2 100644 GIT binary patch delta 1141 zcmcb{eUQ68z?+#xgn@y9gJEV{R`|Z@{*%Ge4FSv07GIlh~F@+%Q~oV{YfQ?8rI2O75pb4LXAg8B&@X=l+b z=Qalgdu>*@_;|N3u^G~KWK-~H()ze-kAzcx#5T%JAg zhc(lNfXSwe0YV`EU%Y-baWl~GSz-(fVhodWSS9M;h6m=~G7zYHzrLfQjl;?D^%A?1 z3u`Z%@UDIPEg*DSa^I1|H(tECuq^cdZ@tE#1(D0c6ib31GJd-Iv*zRXyEpgU{k$hi z{--FvXw5a{;Oh5xh1@ch_?!^6jem0f9@F{U^p_$hf{e{R+JEHyv7Cu<%UAZU<^N7r z-nTz=;>CP}o`iY@M~Az+%hy)hGpsfFwc+LS`@61tPw^JuGgaZ+xvIKZB_yb(@ynSW zNBLJSE2I7?!3FUkv?boS~)gGN(#>Dk(5 zhQ_n{3wMcs;OM%;a<=P$n#}Pndn2Yl%f1%%InajXYeKc~!^PX{_i;$;->&<1KO~}; z>$!OJ;>C=!I5I`cR_%6MZS&OhZ}W3*eT^K~dPmDcr(J~<=O5$F&6#+6PWYqL!bvNC zx!taK{YfAe&`{_{A=`0vMd@7;YLC9zNLVhQ9!&ES(?vS`SI zguwX#nAht;`5uVjpz-x&16CQZ8q|CNSHZy0*f=?xRTZp8W@=KE8c<3Ti20C|Ff?XN NUeBt?wu=QM2ml}T`)L3G delta 1146 zcmX@eeT}<5z?+#xgn@y9gP~P0BfMgN@{#>OUOFR4gn^+rJ2Ou&GcWUKzhT}X2N9Rw zwnvYMdTiwNW0|-+q$EVq*X3j4{&n05!$_KRIWI3VioSNzawdi2AtyJm(a(3BKBk7haZ<3wWnjo?buyeER+OkztP|UQS_? zvFpmdS&@8Kdges87`7?1zn}hdH|j+3-AXIA#hD&cZ^*ykc(Yl6v1JqcoMqom9^F{m zGEpRbp-cS_zQY`|t~F;~{>8RphDdwg-2bz}>>pY=T8K=@Sh6RRcT&UV55h~He_}hb z(8D6%M1C>C!ABHw-e^!(;id$#d> z;S5;!ICj->u=ZyIdqNv9I{;Dyu*TfgLlO zI0cTrd!SfQ`^VyJ(El?o^|h>j>=jNJFDesh(9P#P=~pnv3q~dpqO#f%wDP@6VQ(Z9i^v{GILGinpgeUBAcov1pQu$MKI%(&syJ z`&J)Jy?FjgZBg#uW$Ry-l-+q=ruF;IxnmOX_K#1US+COI#Np7qBCn&1c4?^_^XfdXNA8lsf-^ z{j)GheYq){_ulkJ$q186SpxY`v;E{NEE@74A*4LQzzfVPaM1X4vNWrV0!n5PV_*Ox k1Zb>goIICNVsbt!4>M5V Date: Fri, 10 Nov 2023 16:37:07 +0900 Subject: [PATCH 649/896] Remove width/height bindables from `ArgonWedgePiece` --- osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs | 24 +++----------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs index 085e4c738f..3c2e3e05ea 100644 --- a/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs +++ b/osu.Game/Screens/Play/HUD/ArgonWedgePiece.cs @@ -18,30 +18,14 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } - public float EndOpacity { get; init; } = 0.25f; - - [SettingSource("Wedge width")] - public BindableFloat WedgeWidth { get; } = new BindableFloat(400f) - { - MinValue = 0f, - MaxValue = 500f, - Precision = 1f, - }; - - [SettingSource("Wedge height")] - public BindableFloat WedgeHeight { get; } = new BindableFloat(100f) - { - MinValue = 0f, - MaxValue = 500f, - Precision = 1f, - }; - [SettingSource("Inverted shear")] public BindableBool InvertShear { get; } = new BindableBool(); public ArgonWedgePiece() { CornerRadius = 10f; + + Size = new Vector2(400, 100); } [BackgroundDependencyLoader] @@ -53,7 +37,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(EndOpacity)), + Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66CCFF").Opacity(0.0f), Color4Extensions.FromHex("#66CCFF").Opacity(0.25f)), }; } @@ -61,8 +45,6 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - WedgeWidth.BindValueChanged(v => Width = v.NewValue, true); - WedgeHeight.BindValueChanged(v => Height = v.NewValue, true); InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true); } } From 67312a2db922287fef12c53f00be99b2aa79df8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 16:43:47 +0900 Subject: [PATCH 650/896] Remove `ArgonScoreWedge` and use `ArgonWedgePiece` directly --- osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs | 37 -------------------- osu.Game/Skinning/ArgonSkin.cs | 26 ++++++++------ 2 files changed, 16 insertions(+), 47 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs b/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs deleted file mode 100644 index e8ade9e5c6..0000000000 --- a/osu.Game/Screens/Play/HUD/ArgonScoreWedge.cs +++ /dev/null @@ -1,37 +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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Skinning; -using osuTK; - -namespace osu.Game.Screens.Play.HUD -{ - public partial class ArgonScoreWedge : CompositeDrawable, ISerialisableDrawable - { - public bool UsesFixedAnchor { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - AutoSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - new ArgonWedgePiece - { - WedgeWidth = { Value = 380 }, - WedgeHeight = { Value = 72 }, - }, - new ArgonWedgePiece - { - WedgeWidth = { Value = 380 }, - WedgeHeight = { Value = 72 }, - Position = new Vector2(4, 5) - }, - }; - } - } -} diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index b78abdffd7..ddb375778c 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -117,7 +117,7 @@ namespace osu.Game.Skinning { var health = container.OfType().FirstOrDefault(); var healthLine = container.OfType().FirstOrDefault(); - var scoreWedge = container.OfType().FirstOrDefault(); + var wedgePieces = container.OfType().ToArray(); var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); @@ -144,15 +144,13 @@ namespace osu.Game.Skinning healthLine.Size = new Vector2(45, 3); } - if (scoreWedge != null) - { - scoreWedge.Position = new Vector2(-50, 15); + foreach (var wedgePiece in wedgePieces) + wedgePiece.Position += new Vector2(-50, 15); - if (score != null) - { - score.Origin = Anchor.TopRight; - score.Position = new Vector2(components_x_offset + 200, scoreWedge.Y + 30); - } + if (score != null) + { + score.Origin = Anchor.TopRight; + score.Position = new Vector2(components_x_offset + 200, wedgePieces.Last().Y + 30); } if (accuracy != null) @@ -210,7 +208,15 @@ namespace osu.Game.Skinning { Children = new Drawable[] { - new ArgonScoreWedge(), + new ArgonWedgePiece + { + Size = new Vector2(380, 72), + }, + new ArgonWedgePiece + { + Size = new Vector2(380, 72), + Position = new Vector2(4, 5) + }, new ArgonScoreCounter(), new ArgonHealthDisplay(), new BoxElement(), From a02aeed50af48f580a1c5d7fbb8a792f2a652f1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 16:50:41 +0900 Subject: [PATCH 651/896] Adjust combo animation slightly --- osu.Game/Screens/Play/HUD/ArgonComboCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index 63a17529f5..ac710294ef 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD float newScale = Math.Clamp(text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f); - float duration = wasMiss ? 2000 : 300; + float duration = wasMiss ? 2000 : 500; text.NumberContainer .ScaleTo(new Vector2(newScale)) From 46a219e01010e32827f3772aae77fece7de8bccf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 17:53:02 +0900 Subject: [PATCH 652/896] Add comment explaining `AutoSize` change in `LegacySongProgress` --- osu.Game/Skinning/LegacySongProgress.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Skinning/LegacySongProgress.cs b/osu.Game/Skinning/LegacySongProgress.cs index 26004ff111..fa6ee38fee 100644 --- a/osu.Game/Skinning/LegacySongProgress.cs +++ b/osu.Game/Skinning/LegacySongProgress.cs @@ -21,6 +21,8 @@ namespace osu.Game.Skinning public LegacySongProgress() { + // User shouldn't be able to adjust width/height of this as `CircularProgress` doesn't + // handle stretched cases ell. AutoSizeAxes = Axes.Both; } From f25489cc7be71426891f50046b70aefdc24578de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 17:53:56 +0900 Subject: [PATCH 653/896] Check X/Y sizing available separately to fix weird edge cases --- .../SkinEditor/SkinSelectionHandler.cs | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 77e522a925..a4d530a4d9 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -32,11 +32,11 @@ namespace osu.Game.Overlays.SkinEditor UpdatePosition = updateDrawablePosition }; - private bool allSelectedSupportManualSizing => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes == Axes.None); + private bool allSelectedSupportManualSizing(Axes axis) => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); public override bool HandleScale(Vector2 scale, Anchor anchor) { - bool adjustSize; + Axes adjustAxis; switch (anchor) { @@ -45,19 +45,25 @@ namespace osu.Game.Overlays.SkinEditor case Anchor.TopRight: case Anchor.BottomLeft: case Anchor.BottomRight: - adjustSize = false; + adjustAxis = Axes.Both; break; // for edges, adjust size. + // autosize elements can't be easily handled so just disable sizing for now. case Anchor.TopCentre: - case Anchor.CentreLeft: - case Anchor.CentreRight: case Anchor.BottomCentre: - // autosize elements can't be easily handled so just disable sizing for now. - if (!allSelectedSupportManualSizing) + if (!allSelectedSupportManualSizing(Axes.Y)) return false; - adjustSize = true; + adjustAxis = Axes.Y; + break; + + case Anchor.CentreLeft: + case Anchor.CentreRight: + if (!allSelectedSupportManualSizing(Axes.X)) + return false; + + adjustAxis = Axes.X; break; default: @@ -151,10 +157,20 @@ namespace osu.Game.Overlays.SkinEditor if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90)) currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X); - if (adjustSize) - drawableItem.Size *= currentScaledDelta; - else - drawableItem.Scale *= currentScaledDelta; + switch (adjustAxis) + { + case Axes.X: + drawableItem.Width *= currentScaledDelta.X; + break; + + case Axes.Y: + drawableItem.Height *= currentScaledDelta.Y; + break; + + case Axes.Both: + drawableItem.Scale *= currentScaledDelta; + break; + } } return true; @@ -203,7 +219,8 @@ namespace osu.Game.Overlays.SkinEditor { base.OnSelectionChanged(); - SelectionBox.CanScaleX = SelectionBox.CanScaleY = allSelectedSupportManualSizing; + SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X); + SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y); SelectionBox.CanScaleProportionally = true; SelectionBox.CanFlipX = true; SelectionBox.CanFlipY = true; From d0f1326a63da2187c80c7429cf386bfb3856c8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 17:54:48 +0900 Subject: [PATCH 654/896] Fix formatting --- osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index 93e2583eb7..b38fb9153a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -125,7 +125,9 @@ namespace osu.Game.Tests.Visual.Online Masking = true, EdgeEffect = new EdgeEffectParameters { - Type = EdgeEffectType.Shadow, Radius = 1, Colour = Color4.Black.Opacity(0.2f), + Type = EdgeEffectType.Shadow, + Radius = 1, + Colour = Color4.Black.Opacity(0.2f), }, }; } From 35e11c7c63b83122a45f5f9820aa6909ffb892bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 17:51:30 +0900 Subject: [PATCH 655/896] Rename diagonal scale variable and update xmldoc --- .../Edit/OsuSelectionHandler.cs | 2 +- .../Editing/TestSceneComposeSelectBox.cs | 2 +- .../SkinEditor/SkinSelectionHandler.cs | 2 +- .../Edit/Compose/Components/SelectionBox.cs | 22 +++++++++++-------- osu.sln.DotSettings | 1 + 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 4da640a971..4765f615ce 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; - SelectionBox.CanScaleProportionally = SelectionBox.CanScaleX && SelectionBox.CanScaleY; + SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY; SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 9e8d75efea..f6637d0e80 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing CanScaleX = true, CanScaleY = true, - CanScaleProportionally = true, + CanScaleDiagonally = true, CanFlipX = true, CanFlipY = true, diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index a4d530a4d9..52c012a15a 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -221,7 +221,7 @@ namespace osu.Game.Overlays.SkinEditor SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X); SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y); - SelectionBox.CanScaleProportionally = true; + SelectionBox.CanScaleDiagonally = true; SelectionBox.CanFlipX = true; SelectionBox.CanFlipY = true; SelectionBox.CanReverse = false; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 0917867a61..0b16941bc4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool canScaleX; /// - /// Whether horizontal scaling support should be enabled. + /// Whether horizontal scaling (from the left or right edge) support should be enabled. /// public bool CanScaleX { @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool canScaleY; /// - /// Whether vertical scaling support should be enabled. + /// Whether vertical scaling (from the top or bottom edge) support should be enabled. /// public bool CanScaleY { @@ -91,19 +91,23 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private bool canScaleProportionally; + private bool canScaleDiagonally; /// - /// Whether vertical scaling support should be enabled. + /// Whether diagonal scaling (from a corner) support should be enabled. /// - public bool CanScaleProportionally + /// + /// There are some cases where we only want to allow proportional resizing, and not allow + /// one or both explicit directions of scale. + /// + public bool CanScaleDiagonally { - get => canScaleProportionally; + get => canScaleDiagonally; set { - if (canScaleProportionally == value) return; + if (canScaleDiagonally == value) return; - canScaleProportionally = value; + canScaleDiagonally = value; recreate(); } } @@ -262,7 +266,7 @@ namespace osu.Game.Screens.Edit.Compose.Components }; if (CanScaleX) addXScaleComponents(); - if (CanScaleProportionally) addFullScaleComponents(); + if (CanScaleDiagonally) addFullScaleComponents(); if (CanScaleY) addYScaleComponents(); if (CanFlipX) addXFlipComponents(); if (CanFlipY) addYFlipComponents(); diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index c2778ca5b1..342bc8aa79 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -823,6 +823,7 @@ See the LICENCE file in the repository root for full licence text. True True True + True True True True From 36d0bae42dfe95481b3e20751d8f7032b925e9df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 17:57:16 +0900 Subject: [PATCH 656/896] Restore mention of dependency on another ctor param --- osu.Game/Users/Drawables/UpdateableAvatar.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index b020e7fa63..21153ecfc3 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -54,7 +54,10 @@ namespace osu.Game.Users.Drawables /// /// The initial user to display. /// If set to true, hover/click sounds will play and clicking the avatar will open the user's profile. - /// If set to true, the user status panel will be displayed in the tooltip. + /// + /// If set to true, the user status panel will be displayed in the tooltip. + /// Only has an effect if is true. + /// /// Whether to show a default guest representation on null user (as opposed to nothing). public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUserPanelOnHover = false, bool showGuestOnNull = true) { From 2c1f304f3b4faaa645edb3c138b915ad351c9fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 18:13:36 +0900 Subject: [PATCH 657/896] Fix test failures due to fluctuations in needlessly-serialised automatic sizings --- osu.Game/Skinning/SerialisableDrawableExtensions.cs | 7 +++++-- osu.Game/Skinning/SerialisedDrawableInfo.cs | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs index 0b44db9bdc..609dd9c8ab 100644 --- a/osu.Game/Skinning/SerialisableDrawableExtensions.cs +++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; @@ -19,8 +20,10 @@ namespace osu.Game.Skinning // todo: can probably make this better via deserialisation directly using a common interface. component.Position = drawableInfo.Position; component.Rotation = drawableInfo.Rotation; - if (drawableInfo.Size != Vector2.Zero && (component as CompositeDrawable)?.AutoSizeAxes == Axes.None) - component.Size = drawableInfo.Size; + if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) == false) + component.Width = width; + if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) == false) + component.Height = height; component.Scale = drawableInfo.Scale; component.Anchor = drawableInfo.Anchor; component.Origin = drawableInfo.Origin; diff --git a/osu.Game/Skinning/SerialisedDrawableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs index 0705e91d6d..b2237acc5a 100644 --- a/osu.Game/Skinning/SerialisedDrawableInfo.cs +++ b/osu.Game/Skinning/SerialisedDrawableInfo.cs @@ -35,7 +35,9 @@ namespace osu.Game.Skinning public Vector2 Scale { get; set; } - public Vector2 Size { get; set; } + public float? Width { get; set; } + + public float? Height { get; set; } public Anchor Anchor { get; set; } @@ -64,7 +66,8 @@ namespace osu.Game.Skinning Position = component.Position; Rotation = component.Rotation; Scale = component.Scale; - Size = component.Size; + Height = component.Height; + Width = component.Width; Anchor = component.Anchor; Origin = component.Origin; From 374e13b496dc3780e03f2c9f76a850ab0a8050e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 18:18:45 +0900 Subject: [PATCH 658/896] Remove argon health display bar length setting It is no longer needed. Intentionally not doing backwards migration to simplify things; users can fix their skins. --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 793d43f7ef..b4002343d3 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -35,14 +35,6 @@ namespace osu.Game.Screens.Play.HUD Precision = 1 }; - [SettingSource("Bar length")] - public BindableFloat BarLength { get; } = new BindableFloat(0.98f) - { - MinValue = 0.2f, - MaxValue = 1, - Precision = 0.01f, - }; - private BarPath mainBar = null!; /// @@ -140,7 +132,6 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(_ => Scheduler.AddOnce(updateCurrent), true); - BarLength.BindValueChanged(l => Width = l.NewValue, true); BarHeight.BindValueChanged(_ => updatePath()); updatePath(); } From 43a4b34295471a2a4c0103aa9383ef75cbb8dc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 18:20:14 +0900 Subject: [PATCH 659/896] Fix typo --- osu.Game/Skinning/LegacySongProgress.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySongProgress.cs b/osu.Game/Skinning/LegacySongProgress.cs index fa6ee38fee..4295060a3a 100644 --- a/osu.Game/Skinning/LegacySongProgress.cs +++ b/osu.Game/Skinning/LegacySongProgress.cs @@ -22,7 +22,7 @@ namespace osu.Game.Skinning public LegacySongProgress() { // User shouldn't be able to adjust width/height of this as `CircularProgress` doesn't - // handle stretched cases ell. + // handle stretched cases well. AutoSizeAxes = Axes.Both; } From 26eae0bdeeed6670fc3a47b81abdc75afceda434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 18:25:39 +0900 Subject: [PATCH 660/896] Remove unused using directive --- osu.Game/Skinning/SerialisableDrawableExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Skinning/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs index 609dd9c8ab..6938ed4091 100644 --- a/osu.Game/Skinning/SerialisableDrawableExtensions.cs +++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Extensions; -using osuTK; namespace osu.Game.Skinning { From ec2200d54fff1cf823355571c2a3c827c3a19ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 18:26:08 +0900 Subject: [PATCH 661/896] Remove another reference to bar length --- .../Visual/Gameplay/TestSceneArgonHealthDisplay.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index 7bad623d7f..f51577bc84 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -47,12 +47,6 @@ namespace osu.Game.Tests.Visual.Gameplay }; }); - AddSliderStep("Width", 0, 1f, 1f, val => - { - if (healthDisplay.IsNotNull()) - healthDisplay.BarLength.Value = val; - }); - AddSliderStep("Height", 0, 64, 0, val => { if (healthDisplay.IsNotNull()) From fbf94214a536a5b5aa7246d2ee34ee5ecacf8fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 18:36:09 +0900 Subject: [PATCH 662/896] Fully delegate tooltip show/hide logic --- osu.Game/Users/Drawables/ClickableAvatar.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 6390acc608..ef451df95d 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -105,17 +105,17 @@ namespace osu.Game.Users.Drawables public partial class NoCardTooltip : VisibilityContainer, ITooltip { + private readonly OsuTooltipContainer.OsuTooltip tooltip; + public NoCardTooltip() { - var tooltip = new OsuTooltipContainer.OsuTooltip(); + tooltip = new OsuTooltipContainer.OsuTooltip(); tooltip.SetContent(ContextMenuStrings.ViewProfile); - tooltip.Show(); - Child = tooltip; } - protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); - protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint); + protected override void PopIn() => tooltip.Show(); + protected override void PopOut() => tooltip.Hide(); public void Move(Vector2 pos) => Position = pos; From fecc6f580bf309357682d1ee725f0a3796d9261a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 23 Oct 2023 16:37:26 +0900 Subject: [PATCH 663/896] Add legacy reference health processor --- .../Scoring/LegacyOsuHealthProcessor.cs | 197 ++++++++++++++++++ .../Scoring/DrainingHealthProcessor.cs | 1 - .../Scoring/LegacyDrainingHealthProcessor.cs | 90 ++++++++ 3 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs create mode 100644 osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs new file mode 100644 index 0000000000..103569ffc3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs @@ -0,0 +1,197 @@ +// 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.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + // Reference implementation for osu!stable's HP drain. + public partial class LegacyOsuHealthProcessor : LegacyDrainingHealthProcessor + { + private const double hp_bar_maximum = 200; + private const double hp_combo_geki = 14; + private const double hp_hit_300 = 6; + private const double hp_slider_repeat = 4; + private const double hp_slider_tick = 3; + + private double lowestHpEver; + private double lowestHpEnd; + private double lowestHpComboEnd; + private double hpRecoveryAvailable; + private double hpMultiplierNormal; + private double hpMultiplierComboEnd; + + public LegacyOsuHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60); + lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void Reset(bool storeResults) + { + hpMultiplierNormal = 1; + hpMultiplierComboEnd = 1; + + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.05; + double currentHp; + double currentHpUncapped; + + do + { + currentHp = hp_bar_maximum; + currentHpUncapped = hp_bar_maximum; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + int comboTooLowCount = 0; + string failReason = string.Empty; + + for (int i = 0; i < Beatmap.HitObjects.Count; i++) + { + HitObject h = Beatmap.HitObjects[i]; + + // Find active break (between current and lastTime) + double localLastTime = lastTime; + double breakTime = 0; + + // Subtract any break time from the duration since the last object + if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + { + BreakPeriod e = Beatmap.Breaks[currentBreak]; + + if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) + { + // consider break start equal to object end time for version 8+ since drain stops during this time + breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; + currentBreak++; + } + } + + reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})"; + break; + } + + double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); + double hpOverkill = Math.Max(0, hpReduction - currentHp); + reduceHp(hpReduction); + + if (h is Slider slider) + { + for (int j = 0; j < slider.RepeatCount + 2; j++) + increaseHp(hpMultiplierNormal * hp_slider_repeat); + foreach (var _ in slider.NestedHitObjects.OfType()) + increaseHp(hpMultiplierNormal * hp_slider_tick); + } + else if (h is Spinner spinner) + { + foreach (var _ in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) + increaseHp(hpMultiplierNormal * 1.7); + } + + if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + failReason = $"overkill ({currentHp / hp_bar_maximum} - {hpOverkill / hp_bar_maximum} <= {lowestHpEver / hp_bar_maximum})"; + break; + } + + if (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo) + { + increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); + + if (currentHp < lowestHpComboEnd) + { + if (++comboTooLowCount > 2) + { + hpMultiplierComboEnd *= 1.07; + hpMultiplierNormal *= 1.03; + fail = true; + failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})"; + break; + } + } + } + else + increaseHp(hpMultiplierNormal * hp_hit_300); + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + hpMultiplierComboEnd *= 1.01; + hpMultiplierNormal *= 1.01; + failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})"; + } + + double recovery = (currentHpUncapped - hp_bar_maximum) / Beatmap.HitObjects.Count; + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + hpMultiplierComboEnd *= 1.02; + hpMultiplierNormal *= 1.01; + failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})"; + } + + if (fail) + { + if (Log) + Console.WriteLine($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); + continue; + } + + if (Log) + Console.WriteLine($"PASSED drop {testDrop / hp_bar_maximum}"); + return testDrop / hp_bar_maximum; + } while (true); + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(double amount) + { + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount)); + } + } + } +} diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 592dcbfeb8..2f81aa735e 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Scoring private const double max_health_target = 0.4; private IBeatmap beatmap; - private double gameplayEndTime; private readonly double drainStartTime; diff --git a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs new file mode 100644 index 0000000000..5d2426e4b7 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs @@ -0,0 +1,90 @@ +// 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.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Utils; + +namespace osu.Game.Rulesets.Scoring +{ + /// + /// A which continuously drains health.
+ /// At HP=0, the minimum health reached for a perfect play is 95%.
+ /// At HP=5, the minimum health reached for a perfect play is 70%.
+ /// At HP=10, the minimum health reached for a perfect play is 30%. + ///
+ public abstract partial class LegacyDrainingHealthProcessor : HealthProcessor + { + protected double DrainStartTime { get; } + protected double GameplayEndTime { get; private set; } + + protected IBeatmap Beatmap { get; private set; } + protected PeriodTracker NoDrainPeriodTracker { get; private set; } + + public bool Log { get; set; } + + public double DrainRate { get; private set; } + + /// + /// Creates a new . + /// + /// The time after which draining should begin. + protected LegacyDrainingHealthProcessor(double drainStartTime) + { + DrainStartTime = drainStartTime; + } + + protected override void Update() + { + base.Update(); + + if (NoDrainPeriodTracker?.IsInAny(Time.Current) == true) + return; + + // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time + double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, DrainStartTime, GameplayEndTime); + double currentGameplayTime = Math.Clamp(Time.Current, DrainStartTime, GameplayEndTime); + + Health.Value -= DrainRate * (currentGameplayTime - lastGameplayTime); + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + Beatmap = beatmap; + + if (beatmap.HitObjects.Count > 0) + GameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); + + NoDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period( + beatmap.HitObjects + .Select(hitObject => hitObject.GetEndTime()) + .Where(endTime => endTime <= breakPeriod.StartTime) + .DefaultIfEmpty(double.MinValue) + .Last(), + beatmap.HitObjects + .Select(hitObject => hitObject.StartTime) + .Where(startTime => startTime >= breakPeriod.EndTime) + .DefaultIfEmpty(double.MaxValue) + .First() + ))); + + base.ApplyBeatmap(beatmap); + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + DrainRate = 1; + + if (storeResults) + DrainRate = ComputeDrainRate(); + } + + protected abstract double ComputeDrainRate(); + } +} From 12e5766d5073374b463ad6752a98fd8ed937292c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Nov 2023 16:35:03 +0900 Subject: [PATCH 664/896] Implement OsuHealthProcessor following osu-stable --- .../Scoring/OsuHealthProcessor.cs | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs new file mode 100644 index 0000000000..489b8408ea --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -0,0 +1,229 @@ +// 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.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public partial class OsuHealthProcessor : LegacyDrainingHealthProcessor + { + private double lowestHpEver; + private double lowestHpEnd; + private double hpRecoveryAvailable; + private double hpMultiplierNormal; + + public OsuHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void Reset(bool storeResults) + { + hpMultiplierNormal = 1; + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.00025; + double currentHp; + double currentHpUncapped; + + do + { + currentHp = 1; + currentHpUncapped = 1; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + string failReason = string.Empty; + + for (int i = 0; i < Beatmap.HitObjects.Count; i++) + { + HitObject h = Beatmap.HitObjects[i]; + + // Find active break (between current and lastTime) + double localLastTime = lastTime; + double breakTime = 0; + + // Subtract any break time from the duration since the last object + if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + { + BreakPeriod e = Beatmap.Breaks[currentBreak]; + + if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) + { + // consider break start equal to object end time for version 8+ since drain stops during this time + breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; + currentBreak++; + } + } + + reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + failReason = $"hp too low ({currentHp} < {lowestHpEver})"; + break; + } + + double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); + double hpOverkill = Math.Max(0, hpReduction - currentHp); + reduceHp(hpReduction); + + if (h is Slider slider) + { + foreach (var nested in slider.NestedHitObjects) + increaseHp(nested); + } + else if (h is Spinner spinner) + { + foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) + increaseHp(nested); + } + + if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + failReason = $"overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"; + break; + } + + increaseHp(h); + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + hpMultiplierNormal *= 1.01; + failReason = $"end hp too low ({currentHp} < {lowestHpEnd})"; + } + + double recovery = (currentHpUncapped - 1) / Beatmap.HitObjects.Count; + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + hpMultiplierNormal *= 1.01; + failReason = $"recovery too low ({recovery} < {hpRecoveryAvailable})"; + } + + if (fail) + { + if (Log) + Console.WriteLine($"FAILED drop {testDrop}: {failReason}"); + continue; + } + + if (Log) + Console.WriteLine($"PASSED drop {testDrop}"); + return testDrop; + } while (true); + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(HitObject hitObject) + { + double amount = healthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult); + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); + } + } + + protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.HitObject, result.Type); + + private double healthIncreaseFor(HitObject hitObject, HitResult result) + { + double increase; + + switch (result) + { + case HitResult.SmallTickMiss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14); + + case HitResult.LargeTickMiss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.02, -0.075, -0.14); + + case HitResult.Miss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2); + + case HitResult.SmallTickHit: + // This result is always as a result of the slider tail. + increase = 0.02; + break; + + case HitResult.LargeTickHit: + // This result is either a result of a slider tick or a repeat. + increase = hitObject is SliderTick ? 0.015 : 0.02; + break; + + case HitResult.Meh: + increase = 0.002; + break; + + case HitResult.Ok: + increase = 0.011; + break; + + case HitResult.Good: + increase = 0.024; + break; + + case HitResult.Great: + increase = 0.03; + break; + + case HitResult.Perfect: + // 1.1 * Great. Unused. + increase = 0.033; + break; + + case HitResult.SmallBonus: + increase = 0.0085; + break; + + case HitResult.LargeBonus: + increase = 0.01; + break; + + default: + increase = 0; + break; + } + + return hpMultiplierNormal * increase; + } + } +} From b6dcd7d55f0d421d85e2e97d00ae5704d1fb5487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 18:48:45 +0900 Subject: [PATCH 665/896] Fix tests so that they actually assert something --- .../Online/TestSceneUserClickableAvatar.cs | 67 +++---------------- 1 file changed, 9 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs index b38fb9153a..9edaa841b2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserClickableAvatar.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Testing; +using osu.Game.Graphics.Cursor; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -41,65 +42,15 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestClickableAvatarHover() { - AddStep($"click user {1} with UserGridPanel {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 1) - return; + AddStep("hover avatar with user panel", () => InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1))); + AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddStep("hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddUntilStep("wait for tooltip to hide", () => this.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Hidden); - InputManager.MoveMouseTo(targets[0]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); - - AddStep($"click user {2} with username only. {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 2) - return; - - InputManager.MoveMouseTo(targets[1]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); - - AddStep($"click user {3} with UserGridPanel {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 3) - return; - - InputManager.MoveMouseTo(targets[2]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); - - AddStep($"click null user {4}. {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 4) - return; - - InputManager.MoveMouseTo(targets[3]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); - - AddStep($"click null user {5}. {nameof(ClickableAvatar)}", () => - { - var targets = this.ChildrenOfType().ToList(); - if (targets.Count < 5) - return; - - InputManager.MoveMouseTo(targets[4]); - }); - AddWaitStep("wait for tooltip to show", 5); - AddStep("Hover out", () => InputManager.MoveMouseTo(new Vector2(0))); - AddWaitStep("wait for tooltip to hide", 3); + AddStep("hover avatar without user panel", () => InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(0))); + AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Visible); + AddStep("hover out", () => InputManager.MoveMouseTo(new Vector2(0))); + AddUntilStep("wait for tooltip to hide", () => this.ChildrenOfType().FirstOrDefault()?.State.Value == Visibility.Hidden); } private Drawable generateUser(string username, int id, CountryCode countryCode, string cover, bool showPanel, string? color = null) From 793d90e396bb3268af6117f1fff2a5b474e80e2d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 10 Nov 2023 19:09:09 +0900 Subject: [PATCH 666/896] Add some notes --- osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 489b8408ea..2266cf9d33 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Scoring double localLastTime = lastTime; double breakTime = 0; + // TODO: This doesn't handle overlapping/sequential breaks correctly (/b/614). // Subtract any break time from the duration since the last object if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) { @@ -107,6 +108,8 @@ namespace osu.Game.Rulesets.Osu.Scoring increaseHp(nested); } + // Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners + // will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version. if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) { fail = true; From b7acbde719744a3aac166c676b792e9d87699612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 10 Nov 2023 19:12:26 +0900 Subject: [PATCH 667/896] Only store width/height of serialised drawable if it isn't automatically computed --- osu.Game/Skinning/SerialisedDrawableInfo.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SerialisedDrawableInfo.cs b/osu.Game/Skinning/SerialisedDrawableInfo.cs index b2237acc5a..2d6113ff70 100644 --- a/osu.Game/Skinning/SerialisedDrawableInfo.cs +++ b/osu.Game/Skinning/SerialisedDrawableInfo.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Framework.Bindables; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; @@ -66,8 +67,13 @@ namespace osu.Game.Skinning Position = component.Position; Rotation = component.Rotation; Scale = component.Scale; - Height = component.Height; - Width = component.Width; + + if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) + Width = component.Width; + + if ((component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) + Height = component.Height; + Anchor = component.Anchor; Origin = component.Origin; From 57cd5194ce06b16e03dc0dea32a0e81da80b1029 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 20:00:20 +0900 Subject: [PATCH 668/896] Flip comparison to allow non-composite drawables to still get resized --- osu.Game/Skinning/SerialisableDrawableExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SerialisableDrawableExtensions.cs b/osu.Game/Skinning/SerialisableDrawableExtensions.cs index 6938ed4091..97c4cc8f73 100644 --- a/osu.Game/Skinning/SerialisableDrawableExtensions.cs +++ b/osu.Game/Skinning/SerialisableDrawableExtensions.cs @@ -19,9 +19,9 @@ namespace osu.Game.Skinning // todo: can probably make this better via deserialisation directly using a common interface. component.Position = drawableInfo.Position; component.Rotation = drawableInfo.Rotation; - if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) == false) + if (drawableInfo.Width is float width && width != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.X) != true) component.Width = width; - if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) == false) + if (drawableInfo.Height is float height && height != 0 && (component as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(Axes.Y) != true) component.Height = height; component.Scale = drawableInfo.Scale; component.Anchor = drawableInfo.Anchor; From 2e48569982039c9c1e6cb722190a6669ea09aee7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Nov 2023 20:00:51 +0900 Subject: [PATCH 669/896] Improve test comparison logic Doesn't really help that much with nunit output, but at least you can breakpoint and see the json kinda. --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 4e5db5d46e..bd56a95809 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -4,6 +4,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; +using System.Text.Unicode; +using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -214,7 +217,11 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("Press undo", () => InputManager.Keys(PlatformAction.Undo)); - AddAssert("Nothing changed", () => defaultState.SequenceEqual(changeHandler.GetCurrentState())); + + AddAssert("Nothing changed", + () => JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(defaultState)), + () => Is.EqualTo(JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(changeHandler.GetCurrentState()))) + ); AddStep("Add components", () => { @@ -243,7 +250,11 @@ namespace osu.Game.Tests.Visual.Gameplay void revertAndCheckUnchanged() { AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue)); - AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState())); + + AddAssert("Current state is same as default", + () => JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(defaultState)), + () => Is.EqualTo(JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(changeHandler.GetCurrentState()))) + ); } } From 080f13e34d0f5f60381d813edfd590aa2b466194 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Nov 2023 02:56:16 +0300 Subject: [PATCH 670/896] Schedule outside of `UnloadStoryboard` and fix disposal happening on update thread --- .../Backgrounds/BeatmapBackgroundWithStoryboard.cs | 13 +++++-------- osu.Game/Screens/BackgroundScreenStack.cs | 2 +- .../Screens/Backgrounds/BackgroundScreenDefault.cs | 13 +++++++++++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 1e702967b6..2bde71a6a1 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -71,7 +71,7 @@ namespace osu.Game.Graphics.Backgrounds }, (loadCancellationSource = new CancellationTokenSource()).Token); } - public void UnloadStoryboard(Action scheduleStoryboardRemoval) + public void UnloadStoryboard() { if (drawableStoryboard == null) return; @@ -79,15 +79,12 @@ namespace osu.Game.Graphics.Backgrounds loadCancellationSource.AsNonNull().Cancel(); loadCancellationSource = null; - DrawableStoryboard s = drawableStoryboard; - - scheduleStoryboardRemoval(() => - { - s.RemoveAndDisposeImmediately(); - Sprite.Alpha = 1f; - }); + // clear is intentionally used here for the storyboard to be disposed asynchronously. + storyboardContainer.Clear(); drawableStoryboard = null; + + Sprite.Alpha = 1f; } protected override void LoadComplete() diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index 9af6601aa4..2c7b219791 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.cs @@ -35,6 +35,6 @@ namespace osu.Game.Screens return true; } - internal void ScheduleToTransitionEnd(Action action) => Scheduler.AddDelayed(action, BackgroundScreen.TRANSITION_LENGTH); + internal ScheduledDelegate ScheduleUntilTransitionEnd(Action action) => Scheduler.AddDelayed(action, BackgroundScreen.TRANSITION_LENGTH); } } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 07b1cc6df4..4583b3e4d6 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -73,13 +73,15 @@ namespace osu.Game.Screens.Backgrounds void next() => Next(); } + private ScheduledDelegate storyboardUnloadDelegate; + public override void OnSuspending(ScreenTransitionEvent e) { var backgroundScreenStack = Parent as BackgroundScreenStack; Debug.Assert(backgroundScreenStack != null); if (background is BeatmapBackgroundWithStoryboard storyboardBackground) - storyboardBackground.UnloadStoryboard(backgroundScreenStack.ScheduleToTransitionEnd); + storyboardUnloadDelegate = backgroundScreenStack.ScheduleUntilTransitionEnd(storyboardBackground.UnloadStoryboard); base.OnSuspending(e); } @@ -87,7 +89,14 @@ namespace osu.Game.Screens.Backgrounds public override void OnResuming(ScreenTransitionEvent e) { if (background is BeatmapBackgroundWithStoryboard storyboardBackground) - storyboardBackground.LoadStoryboard(); + { + if (storyboardUnloadDelegate?.Completed == false) + storyboardUnloadDelegate.Cancel(); + else + storyboardBackground.LoadStoryboard(); + + storyboardUnloadDelegate = null; + } base.OnResuming(e); } From bb912bc6161a8785f105e3da29f7e95f83261249 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Nov 2023 02:57:17 +0300 Subject: [PATCH 671/896] Avoid spinning another load thread on initial storyboard load --- .../BeatmapBackgroundWithStoryboard.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 2bde71a6a1..784c8e4b44 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -1,13 +1,11 @@ // 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.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; @@ -48,27 +46,37 @@ namespace osu.Game.Graphics.Backgrounds Volume = { Value = 0 }, }); - LoadStoryboard(); + LoadStoryboard(false); } - public void LoadStoryboard() + public void LoadStoryboard(bool async = true) { Debug.Assert(drawableStoryboard == null); if (!Beatmap.Storyboard.HasDrawable) return; - LoadComponentAsync(drawableStoryboard = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) + drawableStoryboard = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock - }, s => + }; + + if (async) + LoadComponentAsync(drawableStoryboard, finishLoad, (loadCancellationSource = new CancellationTokenSource()).Token); + else + { + LoadComponent(drawableStoryboard); + finishLoad(drawableStoryboard); + } + + void finishLoad(DrawableStoryboard s) { if (Beatmap.Storyboard.ReplacesBackground) Sprite.FadeOut(BackgroundScreen.TRANSITION_LENGTH, Easing.InQuint); storyboardContainer.FadeInFromZero(BackgroundScreen.TRANSITION_LENGTH, Easing.OutQuint); storyboardContainer.Add(s); - }, (loadCancellationSource = new CancellationTokenSource()).Token); + } } public void UnloadStoryboard() @@ -76,7 +84,7 @@ namespace osu.Game.Graphics.Backgrounds if (drawableStoryboard == null) return; - loadCancellationSource.AsNonNull().Cancel(); + loadCancellationSource?.Cancel(); loadCancellationSource = null; // clear is intentionally used here for the storyboard to be disposed asynchronously. From 96da7a07bbd79bc67cbc58196bb38cd6c835c83a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Nov 2023 02:57:29 +0300 Subject: [PATCH 672/896] Add detailed explaination on existence of `ScheduleUntilTransitionEnd` --- osu.Game/Screens/BackgroundScreenStack.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index 2c7b219791..562b212561 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Framework.Threading; +using osu.Game.Screens.Backgrounds; namespace osu.Game.Screens { @@ -35,6 +37,19 @@ namespace osu.Game.Screens return true; } + /// + /// Schedules a delegate to run after 500ms, the time length of a background screen transition. + /// This is used in to dispose of the storyboard once the background screen is completely off-screen. + /// + /// + /// Late storyboard disposals cannot be achieved with any local scheduler from or any component inside it, + /// due to the screen becoming dead at the moment the transition finishes. And, on the frame that it is dead on, it will not receive an , + /// therefore not guaranteeing to dispose the storyboard at any period of time close to the end of the transition. + /// This might require reconsideration framework-side, possibly exposing a "death" event in or all s in general. + /// + /// The delegate + /// + /// internal ScheduledDelegate ScheduleUntilTransitionEnd(Action action) => Scheduler.AddDelayed(action, BackgroundScreen.TRANSITION_LENGTH); } } From 064857c40b7cd18343672fdf103083ba3cb0fdba Mon Sep 17 00:00:00 2001 From: Poyo Date: Fri, 10 Nov 2023 19:57:44 -0800 Subject: [PATCH 673/896] Calculate unstable rate using rate-adjusted offsets --- osu.Game/Rulesets/Judgements/JudgementResult.cs | 5 +++++ .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 1 + osu.Game/Rulesets/Scoring/HitEvent.cs | 11 +++++++++-- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 3 ++- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 2 +- osu.Game/Rulesets/UI/Playfield.cs | 2 +- osu.Game/Screens/Utility/CircleGameplay.cs | 2 +- osu.Game/Screens/Utility/ScrollingGameplay.cs | 2 +- 8 files changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index c67f8b9fd5..603d470954 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -54,6 +54,11 @@ namespace osu.Game.Rulesets.Judgements /// public double TimeAbsolute => RawTime != null ? Math.Min(RawTime.Value, HitObject.GetEndTime() + HitObject.MaximumJudgementOffset) : HitObject.GetEndTime(); + /// + /// The gameplay rate at the time this occurred. + /// + public double GameplayRate { get; internal set; } + /// /// The combo prior to this occurring. /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ce6475d3ce..0843fd5bdc 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -704,6 +704,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Result.RawTime = Time.Current; + Result.GameplayRate = Clock.Rate; if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index cabbf40a7d..afa654318b 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -19,6 +19,11 @@ namespace osu.Game.Rulesets.Scoring ///
public readonly double TimeOffset; + /// + /// The true gameplay rate at the time of the event. + /// + public readonly double GameplayRate; + /// /// The hit result. /// @@ -46,12 +51,14 @@ namespace osu.Game.Rulesets.Scoring ///
/// The time offset from the end of at which the event occurs. /// The . + /// The true gameplay rate at the time of the event. /// The that triggered the event. /// The previous . /// A position corresponding to the event. - public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) + public HitEvent(double timeOffset, double gameplayRate, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) { TimeOffset = timeOffset; + GameplayRate = gameplayRate; Result = result; HitObject = hitObject; LastHitObject = lastHitObject; @@ -63,6 +70,6 @@ namespace osu.Game.Rulesets.Scoring ///
/// The positional offset. /// The new . - public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset); + public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, GameplayRate, Result, HitObject, LastHitObject, positionOffset); } } diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index b4bdd8a1ea..a93385ef43 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -18,7 +18,8 @@ namespace osu.Game.Rulesets.Scoring /// public static double? CalculateUnstableRate(this IEnumerable hitEvents) { - double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. + double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate).ToArray(); return 10 * standardDeviation(timeOffsets); } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 35a7dfe369..4e899479bd 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Scoring /// The to describe. /// The . protected virtual HitEvent CreateHitEvent(JudgementResult result) - => new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null); + => new HitEvent(result.TimeOffset, result.GameplayRate, result.Type, result.HitObject, lastHitObject, null); protected sealed override void RevertResultInternal(JudgementResult result) { diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e9c35555c8..17baf8838c 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -473,7 +473,7 @@ namespace osu.Game.Rulesets.UI private void onNewResult(DrawableHitObject drawable, JudgementResult result) { - Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null); + Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null && result.GameplayRate != 0.0); judgedEntries.Push(drawable.Entry.AsNonNull()); NewResult?.Invoke(drawable, result); diff --git a/osu.Game/Screens/Utility/CircleGameplay.cs b/osu.Game/Screens/Utility/CircleGameplay.cs index d97812acb4..1f970c5121 100644 --- a/osu.Game/Screens/Utility/CircleGameplay.cs +++ b/osu.Game/Screens/Utility/CircleGameplay.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.Utility .FadeOut(duration) .ScaleTo(1.5f, duration); - HitEvent = new HitEvent(Clock.CurrentTime - HitTime, HitResult.Good, new HitObject + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { HitWindows = new HitWindows(), }, null, null); diff --git a/osu.Game/Screens/Utility/ScrollingGameplay.cs b/osu.Game/Screens/Utility/ScrollingGameplay.cs index f1331d8fb2..5038c53b4a 100644 --- a/osu.Game/Screens/Utility/ScrollingGameplay.cs +++ b/osu.Game/Screens/Utility/ScrollingGameplay.cs @@ -186,7 +186,7 @@ namespace osu.Game.Screens.Utility .FadeOut(duration / 2) .ScaleTo(1.5f, duration / 2); - HitEvent = new HitEvent(Clock.CurrentTime - HitTime, HitResult.Good, new HitObject + HitEvent = new HitEvent(Clock.CurrentTime - HitTime, 1.0, HitResult.Good, new HitObject { HitWindows = new HitWindows(), }, null, null); From 21e1d68b15960c448d9876baaec51776febde22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 11 Nov 2023 15:52:24 +0900 Subject: [PATCH 674/896] Remove unused using directive --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index bd56a95809..08019c90e1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using System.Text.Unicode; using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Allocation; From 2428a97d449b5e5f51ea2f9c8805c32521301a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 11 Nov 2023 17:02:21 +0900 Subject: [PATCH 675/896] Fix editor not clearing undo history on skin change --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 5affaedf1d..78c1ea2f0b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -406,7 +406,14 @@ namespace osu.Game.Overlays.SkinEditor cp.Colour = colours.Yellow; }); + changeHandler?.Dispose(); + skins.EnsureMutableSkin(); + + var targetContainer = getTarget(selectedTarget.Value); + + if (targetContainer != null) + changeHandler = new SkinEditorChangeHandler(targetContainer); hasBegunMutating = true; } From b247b94ecbdb5405de16ba489ef7a62b36cd7656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 11 Nov 2023 19:15:20 +0900 Subject: [PATCH 676/896] Revert test changes They were broken because `SerialisedDrawableInfo` is a reference type and as such equality checks will use reference equality rather than member equality. --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 08019c90e1..4e5db5d46e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -4,8 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; -using Newtonsoft.Json; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -216,11 +214,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("Press undo", () => InputManager.Keys(PlatformAction.Undo)); - - AddAssert("Nothing changed", - () => JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(defaultState)), - () => Is.EqualTo(JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(changeHandler.GetCurrentState()))) - ); + AddAssert("Nothing changed", () => defaultState.SequenceEqual(changeHandler.GetCurrentState())); AddStep("Add components", () => { @@ -249,11 +243,7 @@ namespace osu.Game.Tests.Visual.Gameplay void revertAndCheckUnchanged() { AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue)); - - AddAssert("Current state is same as default", - () => JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(defaultState)), - () => Is.EqualTo(JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(changeHandler.GetCurrentState()))) - ); + AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState())); } } From 61d336521da4adc3c8dcc6bacc20a7f23a306d34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Nov 2023 19:48:56 +0900 Subject: [PATCH 677/896] 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 2870696c03..15553510cb 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 f1159f58b9..ef54dd06b4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From b0aa4a4257a5f590a1b65613cbf988b7034d7a9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 11 Nov 2023 20:34:35 +0900 Subject: [PATCH 678/896] Add "export" item to skin editor menu --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 78c1ea2f0b..38eed55241 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -153,6 +154,8 @@ namespace osu.Game.Overlays.SkinEditor Items = new[] { new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), + new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, + new EditorMenuItemSpacer(), new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), new EditorMenuItemSpacer(), new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), From 870e4ce27e751d2a7129d5fe396f2f96e74865bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 11 Nov 2023 20:42:45 +0900 Subject: [PATCH 679/896] Fix argon health display not handling invalidation correctly --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index b56920c99a..f4ce7d1633 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -89,6 +89,13 @@ namespace osu.Game.Screens.Play.HUD public const float MAIN_PATH_RADIUS = 10f; + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + + public ArgonHealthDisplay() + { + AddLayout(drawSizeLayout); + } + [BackgroundDependencyLoader] private void load() { @@ -134,22 +141,11 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(_ => Scheduler.AddOnce(updateCurrent), true); - UseRelativeSize.BindValueChanged(v => - { - RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None; - }, true); + UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true); BarHeight.BindValueChanged(_ => updatePath(), true); } - protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) - { - if ((invalidation & Invalidation.DrawSize) > 0) - updatePath(); - - return base.OnInvalidate(invalidation, source); - } - private void updateCurrent() { if (Current.Value >= GlowBarValue) finishMissDisplay(); @@ -165,6 +161,12 @@ namespace osu.Game.Screens.Play.HUD { base.Update(); + if (!drawSizeLayout.IsValid) + { + updatePath(); + drawSizeLayout.Validate(); + } + mainBar.Alpha = (float)Interpolation.DampContinuously(mainBar.Alpha, Current.Value > 0 ? 1 : 0, 40, Time.Elapsed); glowBar.Alpha = (float)Interpolation.DampContinuously(glowBar.Alpha, GlowBarValue > 0 ? 1 : 0, 40, Time.Elapsed); } From ee56b4d205f916ea9cf45543c72cce34f53b8a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 11 Nov 2023 20:55:47 +0900 Subject: [PATCH 680/896] Use barely better assertion fail message in test --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 4e5db5d46e..92f28288ca 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; @@ -243,7 +244,9 @@ namespace osu.Game.Tests.Visual.Gameplay void revertAndCheckUnchanged() { AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue)); - AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState())); + AddAssert("Current state is same as default", + () => Encoding.UTF8.GetString(defaultState), + () => Is.EqualTo(Encoding.UTF8.GetString(changeHandler.GetCurrentState()))); } } From 50789d2e18a43dd3b2a2e99ea9487851972f35f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 11 Nov 2023 21:28:04 +0900 Subject: [PATCH 681/896] Fix song progress bars not sizing vertically properly --- .../Screens/Play/HUD/ArgonSongProgress.cs | 66 +++++++++++-------- .../Screens/Play/HUD/DefaultSongProgress.cs | 49 ++++++++------ 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs index be2ce3b272..cb38854bca 100644 --- a/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/ArgonSongProgress.cs @@ -19,6 +19,7 @@ namespace osu.Game.Screens.Play.HUD private readonly ArgonSongProgressGraph graph; private readonly ArgonSongProgressBar bar; private readonly Container graphContainer; + private readonly Container content; private const float bar_height = 10; @@ -30,43 +31,50 @@ namespace osu.Game.Screens.Play.HUD public ArgonSongProgress() { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; Masking = true; CornerRadius = 5; - Children = new Drawable[] + + Child = content = new Container { - info = new SongProgressInfo + RelativeSizeAxes = Axes.X, + Children = new Drawable[] { - Origin = Anchor.TopLeft, - Name = "Info", - Anchor = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, - ShowProgress = false - }, - bar = new ArgonSongProgressBar(bar_height) - { - Name = "Seek bar", - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - OnSeek = time => player?.Seek(time), - }, - graphContainer = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Masking = true, - CornerRadius = 5, - Child = graph = new ArgonSongProgressGraph + info = new SongProgressInfo { - Name = "Difficulty graph", - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive + Origin = Anchor.TopLeft, + Name = "Info", + Anchor = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + ShowProgress = false }, - RelativeSizeAxes = Axes.X, - }, + bar = new ArgonSongProgressBar(bar_height) + { + Name = "Seek bar", + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + OnSeek = time => player?.Seek(time), + }, + graphContainer = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Masking = true, + CornerRadius = 5, + Child = graph = new ArgonSongProgressGraph + { + Name = "Difficulty graph", + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive + }, + RelativeSizeAxes = Axes.X, + }, + } }; - RelativeSizeAxes = Axes.X; } [BackgroundDependencyLoader] @@ -100,7 +108,7 @@ namespace osu.Game.Screens.Play.HUD protected override void Update() { base.Update(); - Height = bar.Height + bar_height + info.Height; + content.Height = bar.Height + bar_height + info.Height; graphContainer.Height = bar.Height; } diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs index 202ead2d66..48809796f3 100644 --- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs +++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; @@ -27,6 +28,7 @@ namespace osu.Game.Screens.Play.HUD private readonly DefaultSongProgressBar bar; private readonly DefaultSongProgressGraph graph; private readonly SongProgressInfo info; + private readonly Container content; [SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowGraph), nameof(SongProgressStrings.ShowGraphDescription))] public Bindable ShowGraph { get; } = new BindableBool(true); @@ -37,31 +39,36 @@ namespace osu.Game.Screens.Play.HUD public DefaultSongProgress() { RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; Anchor = Anchor.BottomRight; Origin = Anchor.BottomRight; - Children = new Drawable[] + Child = content = new Container { - info = new SongProgressInfo + RelativeSizeAxes = Axes.X, + Children = new Drawable[] { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - }, - graph = new DefaultSongProgressGraph - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Height = graph_height, - Margin = new MarginPadding { Bottom = bottom_bar_height }, - }, - bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - OnSeek = time => player?.Seek(time), - }, + info = new SongProgressInfo + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + }, + graph = new DefaultSongProgressGraph + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Height = graph_height, + Margin = new MarginPadding { Bottom = bottom_bar_height }, + }, + bar = new DefaultSongProgressBar(bottom_bar_height, graph_height, handle_size) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + OnSeek = time => player?.Seek(time), + }, + } }; } @@ -107,7 +114,7 @@ namespace osu.Game.Screens.Play.HUD float newHeight = bottom_bar_height + graph_height + handle_size.Y + info.Height - graph.Y; if (!Precision.AlmostEquals(Height, newHeight, 5f)) - Height = newHeight; + content.Height = newHeight; } private void updateBarVisibility() From 926636cc035a17543d987ba9a9ba9c47fa5e7f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Wed, 8 Nov 2023 19:43:54 +0900 Subject: [PATCH 682/896] Generalize Bezier curves to BSplines of Nth degree --- .../TestSceneJuiceStreamSelectionBlueprint.cs | 8 +- .../JuiceStreamPathTest.cs | 4 +- .../Mods/TestSceneCatchModPerfect.cs | 2 +- .../Mods/TestSceneCatchModRelax.cs | 2 +- .../TestSceneAutoJuiceStream.cs | 2 +- .../TestSceneCatchModHidden.cs | 2 +- .../TestSceneDrawableHitObjects.cs | 2 +- .../TestSceneJuiceStream.cs | 2 +- .../Blueprints/Components/EditablePath.cs | 2 +- .../Objects/JuiceStreamPath.cs | 2 +- .../Checks/CheckOffscreenObjectsTest.cs | 12 +-- .../Editor/TestSceneObjectMerging.cs | 10 +-- .../TestSceneOsuEditorSelectInvalidPath.cs | 2 +- .../TestScenePathControlPointVisualiser.cs | 36 ++++----- .../TestSceneSliderControlPointPiece.cs | 28 +++---- .../Editor/TestSceneSliderLengthValidity.cs | 8 +- .../TestSceneSliderPlacementBlueprint.cs | 42 +++++------ .../Editor/TestSceneSliderReversal.cs | 4 +- .../TestSceneSliderSelectionBlueprint.cs | 2 +- .../Editor/TestSceneSliderSnapping.cs | 10 +-- .../Editor/TestSceneSliderSplitting.cs | 28 +++---- .../Editor/TestSliderScaling.cs | 2 +- .../Mods/TestSceneOsuModHidden.cs | 10 +-- .../Mods/TestSceneOsuModPerfect.cs | 2 +- .../OsuHitObjectGenerationUtilsTest.cs | 6 +- .../TestSceneHitCircleLateFade.cs | 2 +- .../TestSceneLegacyHitPolicy.cs | 18 ++--- .../TestSceneSlider.cs | 14 ++-- .../TestSceneSliderApplication.cs | 6 +- .../TestSceneSliderFollowCircleInput.cs | 2 +- .../TestSceneSliderInput.cs | 8 +- .../TestSceneSliderSnaking.cs | 6 +- .../TestSceneStartTimeOrderedHitPolicy.cs | 10 +-- .../Components/PathControlPointPiece.cs | 20 +++-- .../Components/PathControlPointVisualiser.cs | 26 +++---- .../Sliders/SliderPlacementBlueprint.cs | 14 ++-- .../Edit/OsuSelectionHandler.cs | 4 +- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../Formats/LegacyBeatmapDecoderTest.cs | 36 ++++----- .../Formats/LegacyBeatmapEncoderTest.cs | 6 +- .../Editing/LegacyEditorBeatmapPatcherTest.cs | 4 +- .../Editing/TestSceneEditorClipboard.cs | 2 +- .../Editing/TestSceneHitObjectComposer.cs | 2 +- .../Gameplay/TestSceneBezierConverter.cs | 40 +++++----- .../TestSceneGameplaySampleTriggerSource.cs | 2 +- .../Visual/Gameplay/TestSceneSliderPath.cs | 73 +++++++++++-------- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 15 ++-- osu.Game/Database/LegacyBeatmapExporter.cs | 4 +- osu.Game/Rulesets/Objects/BezierConverter.cs | 32 ++++---- .../Objects/Legacy/ConvertHitObjectParser.cs | 20 ++--- osu.Game/Rulesets/Objects/SliderPath.cs | 13 ++-- .../Rulesets/Objects/SliderPathExtensions.cs | 6 +- osu.Game/Rulesets/Objects/Types/PathType.cs | 50 ++++++++++++- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 10 +-- 54 files changed, 372 insertions(+), 305 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 05d7a38a95..16b51d414a 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddStep("update hit object path", () => { - hitObject.Path = new SliderPath(PathType.PerfectCurve, new[] + hitObject.Path = new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(100, 100), @@ -190,16 +190,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor [Test] public void TestVertexResampling() { - addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[] + addBlueprintStep(100, 100, new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(100, 100), new Vector2(50, 200), }), 0.5); AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count); - AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); addAddVertexSteps(150, 150); - AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear); + AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.LINEAR); } private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () => diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs index 95b4fdc07e..82f24633b5 100644 --- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests } while (rng.Next(2) != 0); int length = sliderPath.ControlPoints.Count - start + 1; - sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier; + sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.LINEAR : length == 3 ? PathType.PERFECTCURVE : PathType.BEZIER; } while (rng.Next(3) != 0); if (rng.Next(5) == 0) @@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Catch.Tests foreach (var point in sliderPath.ControlPoints) { - Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null); + Assert.That(point.Type, Is.EqualTo(PathType.LINEAR).Or.Null); Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT)); } diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 71df523951..45e7d7aa28 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods var stream = new JuiceStream { StartTime = 1000, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index 5835ccaf78..a161615579 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods { X = CatchPlayfield.CENTER_X, StartTime = 3000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }) } } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 3261fb656e..202f010680 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.HitObjects.Add(new JuiceStream { X = CatchPlayfield.CENTER_X - width / 2, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(width, 0) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index a44575a46e..419a846ec3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests new JuiceStream { StartTime = 1000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }), + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, -192) }), X = CatchPlayfield.WIDTH / 2 } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 11d6419507..9c5cd68201 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Tests { X = xCoords, StartTime = playfieldTime + 1000, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, 200) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs index c31a7ca99f..9a923adaab 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Tests new JuiceStream { X = CatchPlayfield.CENTER_X, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, 100) diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs index df76bf0a8c..86f92d16ca 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/Components/EditablePath.cs @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components path.ConvertFromSliderPath(sliderPath, hitObject.Velocity); // If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices. - if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear)) + if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.LINEAR)) { path.ResampleVertices(hitObject.NestedHitObjects .Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used. diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs index 0633151ddd..57acf7cee2 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStreamPath.cs @@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Catch.Objects for (int i = 1; i < vertices.Count; i++) { - sliderPath.ControlPoints[^1].Type = PathType.Linear; + sliderPath.ControlPoints[^1].Type = PathType.LINEAR; float deltaX = vertices[i].X - lastPosition.X; double length = (vertices[i].Time - currentTime) * velocity; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs index a72aaa966c..8612a8eb57 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = new Vector2(420, 240), Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(new Vector2(-100, 0)) }), } @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = playfield_centre, Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5)) }), } @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = playfield_centre, Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5)) }), StackHeight = 5 @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = new Vector2(0, 0), Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(playfield_centre) }), } @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Position = playfield_centre, Path = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), new PathControlPoint(-playfield_centre) }), } @@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Path = new SliderPath(new[] { // Circular arc shoots over the top of the screen. - new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(0, 0), PathType.PERFECTCURVE), new PathControlPoint(new Vector2(-100, -200)), new PathControlPoint(new Vector2(100, -200)) }), diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs index 8d8386cae1..3d35ab79f7 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectMerging.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor mergeSelection(); AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( - (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle1.Position, pathType: PathType.LINEAR), (pos: circle2.Position, pathType: null))); AddStep("undo", () => Editor.Undo()); @@ -73,11 +73,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var controlPoints = slider.Path.ControlPoints; (Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints.Count + 2]; - args[0] = (circle1.Position, PathType.Linear); + args[0] = (circle1.Position, PathType.LINEAR); for (int i = 0; i < controlPoints.Count; i++) { - args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.Linear : controlPoints[i].Type); + args[i + 1] = (controlPoints[i].Position + slider.Position, i == controlPoints.Count - 1 ? PathType.LINEAR : controlPoints[i].Type); } args[^1] = (circle2.Position, null); @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor mergeSelection(); AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( - (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle1.Position, pathType: PathType.LINEAR), (pos: circle2.Position, pathType: null))); AddAssert("samples exist", sliderSampleExist); @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor mergeSelection(); AddAssert("slider created", () => circle1 is not null && circle2 is not null && sliderCreatedFor( - (pos: circle1.Position, pathType: PathType.Linear), + (pos: circle1.Position, pathType: PathType.LINEAR), (pos: circle2.Position, pathType: null))); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs index 37a109de18..7ea4d40b90 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(0), PathType.PERFECTCURVE), new PathControlPoint(new Vector2(-100, 0)), new PathControlPoint(new Vector2(100, 20)) }; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index c267cd1f63..16800997f4 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(200), PathType.BEZIER); addControlPointStep(new Vector2(300)); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200)); @@ -63,9 +63,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); addContextMenuItemStep("Perfect curve"); - assertControlPointPathType(0, PathType.Bezier); - assertControlPointPathType(1, PathType.PerfectCurve); - assertControlPointPathType(3, PathType.Bezier); + assertControlPointPathType(0, PathType.BEZIER); + assertControlPointPathType(1, PathType.PERFECTCURVE); + assertControlPointPathType(3, PathType.BEZIER); } [Test] @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(200), PathType.BEZIER); addControlPointStep(new Vector2(300)); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200)); @@ -83,8 +83,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true); addContextMenuItemStep("Perfect curve"); - assertControlPointPathType(0, PathType.Bezier); - assertControlPointPathType(2, PathType.PerfectCurve); + assertControlPointPathType(0, PathType.BEZIER); + assertControlPointPathType(2, PathType.PERFECTCURVE); assertControlPointPathType(4, null); } @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Bezier); + addControlPointStep(new Vector2(200), PathType.BEZIER); addControlPointStep(new Vector2(300)); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200)); @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); addContextMenuItemStep("Perfect curve"); - assertControlPointPathType(0, PathType.Bezier); + assertControlPointPathType(0, PathType.BEZIER); AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null); } @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Linear); + addControlPointStep(new Vector2(200), PathType.LINEAR); addControlPointStep(new Vector2(300)); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200)); @@ -123,9 +123,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true); addContextMenuItemStep("Perfect curve"); - assertControlPointPathType(0, PathType.Linear); - assertControlPointPathType(1, PathType.PerfectCurve); - assertControlPointPathType(3, PathType.Linear); + assertControlPointPathType(0, PathType.LINEAR); + assertControlPointPathType(1, PathType.PERFECTCURVE); + assertControlPointPathType(3, PathType.LINEAR); } [Test] @@ -133,18 +133,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { createVisualiser(true); - addControlPointStep(new Vector2(200), PathType.Bezier); - addControlPointStep(new Vector2(300), PathType.PerfectCurve); + addControlPointStep(new Vector2(200), PathType.BEZIER); + addControlPointStep(new Vector2(300), PathType.PERFECTCURVE); addControlPointStep(new Vector2(500, 300)); - addControlPointStep(new Vector2(700, 200), PathType.Bezier); + addControlPointStep(new Vector2(700, 200), PathType.BEZIER); addControlPointStep(new Vector2(500, 100)); moveMouseToControlPoint(3); AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true); addContextMenuItemStep("Inherit"); - assertControlPointPathType(0, PathType.Bezier); - assertControlPointPathType(1, PathType.Bezier); + assertControlPointPathType(0, PathType.BEZIER); + assertControlPointPathType(1, PathType.BEZIER); assertControlPointPathType(3, null); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index 408205d6b2..1d8d2cf01a 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(256, 192), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECTCURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(1, new Vector2(150, 50)); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); } [Test] @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("three control point pieces selected", () => this.ChildrenOfType>().Count(piece => piece.IsSelected.Value) == 3); assertControlPointPosition(2, new Vector2(450, 50)); - assertControlPointType(2, PathType.PerfectCurve); + assertControlPointType(2, PathType.PERFECTCURVE); assertControlPointPosition(3, new Vector2(550, 50)); @@ -249,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider moved", () => Precision.AlmostEquals(slider.Position, new Vector2(256, 192) + new Vector2(150, 50))); assertControlPointPosition(0, Vector2.Zero); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); assertControlPointPosition(1, new Vector2(0, 100)); @@ -272,7 +272,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(1, new Vector2(400, 0.01f)); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -282,13 +282,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); addMovementStep(new Vector2(400, 0.01f)); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); addMovementStep(new Vector2(150, 50)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(1, new Vector2(150, 50)); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); } [Test] @@ -298,32 +298,32 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); addMovementStep(new Vector2(350, 0.01f)); - assertControlPointType(2, PathType.Bezier); + assertControlPointType(2, PathType.BEZIER); addMovementStep(new Vector2(150, 150)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(4, new Vector2(150, 150)); - assertControlPointType(2, PathType.PerfectCurve); + assertControlPointType(2, PathType.PERFECTCURVE); } [Test] public void TestDragControlPointPathAfterChangingType() { - AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.Bezier); + AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.BEZIER); AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10)))); - AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PerfectCurve); + AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PERFECTCURVE); moveMouseToControlPoint(4); AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); - assertControlPointType(3, PathType.PerfectCurve); + assertControlPointType(3, PathType.PERFECTCURVE); addMovementStep(new Vector2(350, 0.01f)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(4, new Vector2(350, 0.01f)); - assertControlPointType(3, PathType.Bezier); + assertControlPointType(3, PathType.BEZIER); } private void addMovementStep(Vector2 relativePosition) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs index 77e828e80a..38ebeb7e8f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.Linear), + new PathControlPoint(new Vector2(0), PathType.LINEAR), new PathControlPoint(new Vector2(100, 0)), }; @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.Linear), + new PathControlPoint(new Vector2(0), PathType.LINEAR), new PathControlPoint(new Vector2(100, 0)), }; @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(0), PathType.PERFECTCURVE), new PathControlPoint(new Vector2(100, 0)), new PathControlPoint(new Vector2(0, 10)) }; @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.Linear), + new PathControlPoint(new Vector2(0), PathType.LINEAR), new PathControlPoint(new Vector2(0, 50)), new PathControlPoint(new Vector2(0, 100)) }; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 7d29670daa..4b120c1a3f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(200); assertControlPointCount(2); - assertControlPointType(0, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); } [Test] @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(2); - assertControlPointType(0, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); } [Test] @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); } [Test] @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(4); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100, 100)); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -131,8 +131,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.Linear); - assertControlPointType(1, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); + assertControlPointType(1, PathType.LINEAR); } [Test] @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(2); - assertControlPointType(0, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); assertLength(100); } @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); } [Test] @@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(4); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -216,8 +216,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); - assertControlPointType(0, PathType.Linear); - assertControlPointType(1, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); + assertControlPointType(1, PathType.LINEAR); } [Test] @@ -240,8 +240,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointCount(4); assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); - assertControlPointType(0, PathType.Linear); - assertControlPointType(1, PathType.PerfectCurve); + assertControlPointType(0, PathType.LINEAR); + assertControlPointType(1, PathType.PERFECTCURVE); } [Test] @@ -269,8 +269,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(3, new Vector2(200, 100)); assertControlPointPosition(4, new Vector2(200)); - assertControlPointType(0, PathType.PerfectCurve); - assertControlPointType(2, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(2, PathType.PERFECTCURVE); } [Test] @@ -287,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(200); assertControlPointCount(2); - assertControlPointType(0, PathType.Linear); + assertControlPointType(0, PathType.LINEAR); } [Test] @@ -306,7 +306,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -326,7 +326,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); } [Test] @@ -347,7 +347,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); } [Test] @@ -368,7 +368,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.Bezier); + assertControlPointType(0, PathType.BEZIER); } [Test] @@ -385,7 +385,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PerfectCurve); + assertControlPointType(0, PathType.PERFECTCURVE); } private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs index 9c5eb83e3c..0ddfc40946 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs @@ -22,12 +22,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private readonly PathControlPoint[][] paths = { createPathSegment( - PathType.PerfectCurve, + PathType.PERFECTCURVE, new Vector2(200, -50), new Vector2(250, 0) ), createPathSegment( - PathType.Linear, + PathType.LINEAR, new Vector2(100, 0), new Vector2(100, 100) ) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index 413a3c3dfd..d4d99e1019 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor slider = new Slider { Position = new Vector2(256, 192), - Path = new SliderPath(PathType.Bezier, new[] + Path = new SliderPath(PathType.BEZIER, new[] { Vector2.Zero, new Vector2(150, 150), diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs index 0ae14bdde8..c984d9168e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { ControlPoints = { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), new PathControlPoint(new Vector2(136, 205)), new PathControlPoint(new Vector2(-4, 226)) } @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { OsuSelectionHandler selectionHandler; - AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("rotate 90 degrees ccw", () => @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor selectionHandler.HandleRotation(-90); }); - AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); } [Test] @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { OsuSelectionHandler selectionHandler; - AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("flip slider horizontally", () => @@ -232,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor selectionHandler.OnPressed(new KeyBindingPressEvent(InputManager.CurrentState, GlobalAction.EditorFlipVertically)); }); - AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PerfectCurve); + AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); } [Test] diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index ad37258c9b..cded9165f4 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(0, 50), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECTCURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) @@ -73,20 +73,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, - (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(0, 50), PathType.PERFECTCURVE), (new Vector2(150, 200), null), (new Vector2(300, 50), null) ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap, - (new Vector2(300, 50), PathType.PerfectCurve), + (new Vector2(300, 50), PathType.PERFECTCURVE), (new Vector2(400, 50), null), (new Vector2(400, 200), null) )); AddStep("undo", () => Editor.Undo()); AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime, - (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(0, 50), PathType.PERFECTCURVE), (new Vector2(150, 200), null), - (new Vector2(300, 50), PathType.PerfectCurve), + (new Vector2(300, 50), PathType.PERFECTCURVE), (new Vector2(400, 50), null), (new Vector2(400, 200), null) )); @@ -104,11 +104,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(0, 50), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.Bezier), + new PathControlPoint(new Vector2(300, 0), PathType.BEZIER), new PathControlPoint(new Vector2(400, 0)), - new PathControlPoint(new Vector2(400, 150), PathType.Catmull), + new PathControlPoint(new Vector2(400, 150), PathType.CATMULL), new PathControlPoint(new Vector2(300, 200)), new PathControlPoint(new Vector2(400, 250)) }) @@ -139,15 +139,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, - (new Vector2(0, 50), PathType.PerfectCurve), + (new Vector2(0, 50), PathType.PERFECTCURVE), (new Vector2(150, 200), null), (new Vector2(300, 50), null) ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap, - (new Vector2(300, 50), PathType.Bezier), + (new Vector2(300, 50), PathType.BEZIER), (new Vector2(400, 50), null), (new Vector2(400, 200), null) ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[2], EditorBeatmap.HitObjects[1].GetEndTime() + split_gap, endTime + split_gap * 2, - (new Vector2(400, 200), PathType.Catmull), + (new Vector2(400, 200), PathType.CATMULL), (new Vector2(300, 250), null), (new Vector2(400, 300), null) )); @@ -165,9 +165,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(0, 50), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PerfectCurve), + new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECTCURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs index 64d23090d0..021fdba225 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSliderScaling.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.Linear), + new PathControlPoint(new Vector2(0), PathType.LINEAR), new PathControlPoint(new Vector2(100, 0)), }; diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 3f84ac6935..58bdd805c1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods new Slider { StartTime = 3200, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) }, new Slider { StartTime = 5200, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) } } }, @@ -105,12 +105,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods new Slider { StartTime = 1000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) }, new Slider { StartTime = 4000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) }, } }, @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { StartTime = 3000, Position = new Vector2(156, 242), - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(200, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(200, 0), }) }, new Spinner { diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs index f0496efc19..26c4133bc4 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModPerfect.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods var slider = new Slider { StartTime = 1000, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(100, 0), }) }; CreateHitObjectTest(new HitObjectTestData(slider), shouldMiss); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs index daa914cac2..d78c32aa6a 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuHitObjectGenerationUtilsTest.cs @@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Osu.Tests { ControlPoints = { - new PathControlPoint(new Vector2(), PathType.Linear), - new PathControlPoint(new Vector2(-64, -128), PathType.Linear), // absolute position: (64, 0) - new PathControlPoint(new Vector2(-128, 0), PathType.Linear) // absolute position: (0, 128) + new PathControlPoint(new Vector2(), PathType.LINEAR), + new PathControlPoint(new Vector2(-64, -128), PathType.LINEAR), // absolute position: (64, 0) + new PathControlPoint(new Vector2(-128, 0), PathType.LINEAR) // absolute position: (0, 128) } }, RepeatCount = 1 diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs index 483155e646..7824f26251 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs @@ -167,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 500, Position = new Vector2(250), - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, 100), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs index fa6aa580a3..e460da9bd5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyHitPolicy.cs @@ -264,7 +264,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(50, 0), @@ -308,7 +308,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(50, 0), @@ -391,7 +391,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -428,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_first_slider, Position = positionFirstSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -438,7 +438,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_second_slider, Position = positionSecondSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -521,7 +521,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_first_slider, Position = positionFirstSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -531,7 +531,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_second_slider, Position = positionSecondSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -571,7 +571,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_first_slider, Position = positionFirstSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -581,7 +581,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_second_slider, Position = positionSecondSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index b805e7ed63..60003e7950 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(239, 176), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(154, 28), @@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Tests SliderVelocityMultiplier = speedMultiplier, StartTime = Time.Current + time_offset, Position = new Vector2(0, -(distance / 2)), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(0, distance), @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 2, 0), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(max_length / 2, max_length / 2), @@ -293,7 +293,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 2, 0), - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(max_length * 0.375f, max_length * 0.18f), @@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 2, 0), - Path = new SliderPath(PathType.Bezier, new[] + Path = new SliderPath(PathType.BEZIER, new[] { Vector2.Zero, new Vector2(max_length * 0.375f, max_length * 0.18f), @@ -338,7 +338,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(0, 0), - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(-max_length / 2, 0), @@ -365,7 +365,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 4, 0), - Path = new SliderPath(PathType.Catmull, new[] + Path = new SliderPath(PathType.CATMULL, new[] { Vector2.Zero, new Vector2(max_length * 0.125f, max_length * 0.125f), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index 88b70a8836..f41dd913ab 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), IndexInCurrentCombo = 0, StartTime = Time.Current, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(150, 100), @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), ComboIndex = 1, StartTime = dho.HitObject.StartTime, - Path = new SliderPath(PathType.Bezier, new[] + Path = new SliderPath(PathType.BEZIER, new[] { Vector2.Zero, new Vector2(150, 100), @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), IndexInCurrentCombo = 0, StartTime = Time.Current, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(150, 100), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs index d4bb789a12..fc9bb16cb7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderFollowCircleInput.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests StartTime = time_slider_start, Position = new Vector2(0, 0), SliderVelocityMultiplier = velocity, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(followCircleRadius, 0), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index f718a5069f..08836ef819 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(0, 0), SliderVelocityMultiplier = 10f, RepeatCount = repeatCount, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(sliderLength, 0), @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(0, 0), SliderVelocityMultiplier = 10f, RepeatCount = repeatCount, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(sliderLength, 0), @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests StartTime = time_slider_start, Position = new Vector2(0, 0), SliderVelocityMultiplier = 10f, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(slider_path_length * 10, 0), @@ -478,7 +478,7 @@ namespace osu.Game.Rulesets.Osu.Tests StartTime = time_slider_start, Position = new Vector2(0, 0), SliderVelocityMultiplier = 0.1f, - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(slider_path_length, 0), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 13166c2b6b..ebc5143aed 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 3000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(300, 200) @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 13000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(300, 200) @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 23000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PerfectCurve, new[] + Path = new SliderPath(PathType.PERFECTCURVE, new[] { Vector2.Zero, new Vector2(300, 200) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 3475680c71..895e9bbdee 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -318,7 +318,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider, Position = positionSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -352,7 +352,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_first_slider, Position = positionFirstSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), @@ -362,7 +362,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_second_slider, Position = positionSecondSlider, - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(25, 0), diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 12e5ca0236..9658e5f6c3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -221,11 +221,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components ///
private void updatePathType() { - if (ControlPoint.Type != PathType.PerfectCurve) + if (ControlPoint.Type != PathType.PERFECTCURVE) return; if (PointsInSegment.Count > 3) - ControlPoint.Type = PathType.Bezier; + ControlPoint.Type = PathType.BEZIER; if (PointsInSegment.Count != 3) return; @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components ReadOnlySpan points = PointsInSegment.Select(p => p.Position).ToArray(); RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); if (boundingBox.Width >= 640 || boundingBox.Height >= 480) - ControlPoint.Type = PathType.Bezier; + ControlPoint.Type = PathType.BEZIER; } /// @@ -256,18 +256,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private Color4 getColourFromNodeType() { - if (!(ControlPoint.Type is PathType pathType)) + if (ControlPoint.Type is not PathType pathType) return colours.Yellow; switch (pathType) { - case PathType.Catmull: + case { SplineType: SplineType.Catmull }: return colours.SeaFoam; - case PathType.Bezier: - return colours.Pink; + case { SplineType: SplineType.BSpline, Degree: null }: + return colours.PinkLighter; - case PathType.PerfectCurve: + case { SplineType: SplineType.BSpline, Degree: >= 1 }: + int idx = Math.Clamp(pathType.Degree.Value, 0, 3); + return new[] { colours.PinkDarker, colours.PinkDark, colours.Pink, colours.PinkLight }[idx]; + + case { SplineType: SplineType.PerfectCurve }: return colours.PurpleDark; default: diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f891d23bbd..b5c9016538 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -242,18 +242,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); - switch (type) + if (type.HasValue && type.Value.SplineType == SplineType.PerfectCurve) { - case PathType.PerfectCurve: - // Can't always create a circular arc out of 4 or more points, - // so we split the segment into one 3-point circular arc segment - // and one segment of the previous type. - int thirdPointIndex = indexInSegment + 2; + // Can't always create a circular arc out of 4 or more points, + // so we split the segment into one 3-point circular arc segment + // and one segment of the previous type. + int thirdPointIndex = indexInSegment + 2; - if (piece.PointsInSegment.Count > thirdPointIndex + 1) - piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type; - - break; + if (piece.PointsInSegment.Count > thirdPointIndex + 1) + piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type; } hitObject.Path.ExpectedDistance.Value = null; @@ -370,10 +367,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems.Add(createMenuItemForPathType(null)); // todo: hide/disable items which aren't valid for selected points - curveTypeItems.Add(createMenuItemForPathType(PathType.Linear)); - curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve)); - curveTypeItems.Add(createMenuItemForPathType(PathType.Bezier)); - curveTypeItems.Add(createMenuItemForPathType(PathType.Catmull)); + curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR)); + curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECTCURVE)); + curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER)); + curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3))); + curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); var menuItems = new List { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 9b6adc04cf..8f0a2ee781 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { RelativeSizeAxes = Axes.Both; - HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.Linear)); + HitObject.Path.ControlPoints.Add(segmentStart = new PathControlPoint(Vector2.Zero, PathType.LINEAR)); currentSegmentLength = 1; } @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders Debug.Assert(lastPoint != null); segmentStart = lastPoint; - segmentStart.Type = PathType.Linear; + segmentStart.Type = PathType.LINEAR; currentSegmentLength = 1; } @@ -173,15 +173,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { case 1: case 2: - segmentStart.Type = PathType.Linear; + segmentStart.Type = PathType.LINEAR; break; case 3: - segmentStart.Type = PathType.PerfectCurve; + segmentStart.Type = PathType.PERFECTCURVE; break; default: - segmentStart.Type = PathType.Bezier; + segmentStart.Type = PathType.BEZIER; break; } } @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = Vector2.Zero }); - // The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier). + // The path type should be adjusted in the progression of updatePathType() (LINEAR -> PC -> BEZIER). currentSegmentLength++; updatePathType(); } @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders HitObject.Path.ControlPoints.Remove(cursor); cursor = null; - // The path type should be adjusted in the reverse progression of updatePathType() (Bezier -> PC -> Linear). + // The path type should be adjusted in the reverse progression of updatePathType() (BEZIER -> PC -> LINEAR). currentSegmentLength--; updatePathType(); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index e81941d254..b972f09136 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -320,7 +320,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (mergedHitObject.Path.ControlPoints.Count == 0) { - mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.Linear)); + mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.LINEAR)); } // Merge all the selected hit objects into one slider path. @@ -350,7 +350,7 @@ namespace osu.Game.Rulesets.Osu.Edit // Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type. if (!lastCircle) { - mergedHitObject.Path.ControlPoints.Last().Type = PathType.Linear; + mergedHitObject.Path.ControlPoints.Last().Type = PathType.LINEAR; } mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position)); diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 5f47d486e6..2a76782a08 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Objects double IHasDistance.Distance => Duration * Velocity; SliderPath IHasPath.Path - => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER); + => new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER); #endregion } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 66151a51e6..18c21046fb 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -663,7 +663,7 @@ namespace osu.Game.Tests.Beatmaps.Formats assertObjectHasBanks(hitObjects[9], HitSampleInfo.BANK_DRUM, HitSampleInfo.BANK_NORMAL); } - void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null) + static void assertObjectHasBanks(HitObject hitObject, string normalBank, string? additionsBank = null) { Assert.AreEqual(normalBank, hitObject.Samples[0].Bank); @@ -808,14 +808,14 @@ namespace osu.Game.Tests.Beatmaps.Formats var first = ((IHasPath)decoded.HitObjects[0]).Path; Assert.That(first.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve)); + Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECTCURVE)); Assert.That(first.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244))); Assert.That(first.ControlPoints[1].Type, Is.EqualTo(null)); // ReSharper disable once HeuristicUnreachableCode // weird one, see https://youtrack.jetbrains.com/issue/RIDER-70159. Assert.That(first.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3))); - Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(first.ControlPoints[3].Position, Is.EqualTo(new Vector2(68, 15))); Assert.That(first.ControlPoints[3].Type, Is.EqualTo(null)); Assert.That(first.ControlPoints[4].Position, Is.EqualTo(new Vector2(259, -132))); @@ -827,7 +827,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var second = ((IHasPath)decoded.HitObjects[1]).Path; Assert.That(second.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve)); + Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECTCURVE)); Assert.That(second.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244))); Assert.That(second.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(second.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3))); @@ -837,14 +837,14 @@ namespace osu.Game.Tests.Beatmaps.Formats var third = ((IHasPath)decoded.HitObjects[2]).Path; Assert.That(third.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(third.ControlPoints[1].Position, Is.EqualTo(new Vector2(0, 192))); Assert.That(third.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(third.ControlPoints[2].Position, Is.EqualTo(new Vector2(224, 192))); Assert.That(third.ControlPoints[2].Type, Is.EqualTo(null)); Assert.That(third.ControlPoints[3].Position, Is.EqualTo(new Vector2(224, 0))); - Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(third.ControlPoints[4].Position, Is.EqualTo(new Vector2(224, -192))); Assert.That(third.ControlPoints[4].Type, Is.EqualTo(null)); Assert.That(third.ControlPoints[5].Position, Is.EqualTo(new Vector2(480, -192))); @@ -856,7 +856,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var fourth = ((IHasPath)decoded.HitObjects[3]).Path; Assert.That(fourth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(fourth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1))); Assert.That(fourth.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(fourth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2))); @@ -870,7 +870,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var fifth = ((IHasPath)decoded.HitObjects[4]).Path; Assert.That(fifth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(fifth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1))); Assert.That(fifth.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2))); @@ -881,7 +881,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(fifth.ControlPoints[4].Type, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[5].Position, Is.EqualTo(new Vector2(4, 4))); - Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.Bezier)); + Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.BEZIER)); Assert.That(fifth.ControlPoints[6].Position, Is.EqualTo(new Vector2(5, 5))); Assert.That(fifth.ControlPoints[6].Type, Is.EqualTo(null)); @@ -889,12 +889,12 @@ namespace osu.Game.Tests.Beatmaps.Formats var sixth = ((IHasPath)decoded.HitObjects[5]).Path; Assert.That(sixth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(sixth.ControlPoints[0].Type == PathType.Bezier); + Assert.That(sixth.ControlPoints[0].Type == PathType.BEZIER); Assert.That(sixth.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145))); Assert.That(sixth.ControlPoints[1].Type == null); Assert.That(sixth.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75))); - Assert.That(sixth.ControlPoints[2].Type == PathType.Bezier); + Assert.That(sixth.ControlPoints[2].Type == PathType.BEZIER); Assert.That(sixth.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145))); Assert.That(sixth.ControlPoints[3].Type == null); Assert.That(sixth.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20))); @@ -904,12 +904,12 @@ namespace osu.Game.Tests.Beatmaps.Formats var seventh = ((IHasPath)decoded.HitObjects[6]).Path; Assert.That(seventh.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(seventh.ControlPoints[0].Type == PathType.PerfectCurve); + Assert.That(seventh.ControlPoints[0].Type == PathType.PERFECTCURVE); Assert.That(seventh.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145))); Assert.That(seventh.ControlPoints[1].Type == null); Assert.That(seventh.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75))); - Assert.That(seventh.ControlPoints[2].Type == PathType.PerfectCurve); + Assert.That(seventh.ControlPoints[2].Type == PathType.PERFECTCURVE); Assert.That(seventh.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145))); Assert.That(seventh.ControlPoints[3].Type == null); Assert.That(seventh.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20))); @@ -1016,7 +1016,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints; Assert.That(controlPoints.Count, Is.EqualTo(6)); - Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints.Single(c => c.Type != null).Type, Is.EqualTo(PathType.CATMULL)); } } @@ -1032,9 +1032,9 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints; Assert.That(controlPoints.Count, Is.EqualTo(4)); - Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull)); - Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.Catmull)); - Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.CATMULL)); + Assert.That(controlPoints[1].Type, Is.EqualTo(PathType.CATMULL)); + Assert.That(controlPoints[2].Type, Is.EqualTo(PathType.CATMULL)); Assert.That(controlPoints[3].Type, Is.Null); } } @@ -1051,7 +1051,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var controlPoints = ((IHasPath)decoded.HitObjects[0]).Path.ControlPoints; Assert.That(controlPoints.Count, Is.EqualTo(4)); - Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.Catmull)); + Assert.That(controlPoints[0].Type, Is.EqualTo(PathType.CATMULL)); Assert.That(controlPoints[0].Position, Is.EqualTo(Vector2.Zero)); Assert.That(controlPoints[1].Type, Is.Null); Assert.That(controlPoints[1].Position, Is.Not.EqualTo(Vector2.Zero)); diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 5d9049ead7..db50273f27 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Beatmaps.Formats compareBeatmaps(decoded, decodedAfterEncode); - ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo) + static ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo) { // emulate non-legacy control points by cloning the non-legacy portion. // the assertion is that the encoder can recreate this losslessly from hitobject data. @@ -125,10 +125,10 @@ namespace osu.Game.Tests.Beatmaps.Formats Position = new Vector2(0.6f), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.Bezier), + new PathControlPoint(Vector2.Zero, PathType.BEZIER), new PathControlPoint(new Vector2(0.5f)), new PathControlPoint(new Vector2(0.51f)), // This is actually on the same position as the previous one in legacy beatmaps (truncated to int). - new PathControlPoint(new Vector2(1f), PathType.Bezier), + new PathControlPoint(new Vector2(1f), PathType.BEZIER), new PathControlPoint(new Vector2(2f)) }) }, diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index 5af0366e6e..21d8a165ff 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -162,7 +162,7 @@ namespace osu.Game.Tests.Editing { new PathControlPoint(Vector2.Zero), new PathControlPoint(Vector2.One), - new PathControlPoint(new Vector2(2), PathType.Bezier), + new PathControlPoint(new Vector2(2), PathType.BEZIER), new PathControlPoint(new Vector2(3)), }, 50) }, @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Editing StartTime = 2000, Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.Bezier), + new PathControlPoint(Vector2.Zero, PathType.BEZIER), new PathControlPoint(new Vector2(4)), new PathControlPoint(new Vector2(5)), }, 100) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs index c4c05278b5..a766b253aa 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editing ControlPoints = { new PathControlPoint(), - new PathControlPoint(new Vector2(100, 0), PathType.Bezier) + new PathControlPoint(new Vector2(100, 0), PathType.BEZIER) } } }; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index ed3bffe5c2..f392841ac7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Editing new Slider { Position = new Vector2(128, 256), - Path = new SliderPath(PathType.Linear, new[] + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(216, 0), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs index a40eab5948..5eb82ccbdc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs @@ -114,23 +114,25 @@ namespace osu.Game.Tests.Visual.Gameplay { } - [TestCase(PathType.Linear)] - [TestCase(PathType.Bezier)] - [TestCase(PathType.Catmull)] - [TestCase(PathType.PerfectCurve)] - public void TestSingleSegment(PathType type) - => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + [TestCase(SplineType.Linear, null)] + [TestCase(SplineType.BSpline, null)] + [TestCase(SplineType.BSpline, 3)] + [TestCase(SplineType.Catmull, null)] + [TestCase(SplineType.PerfectCurve, null)] + public void TestSingleSegment(SplineType splineType, int? degree) + => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(new PathType { SplineType = splineType, Degree = degree }, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); - [TestCase(PathType.Linear)] - [TestCase(PathType.Bezier)] - [TestCase(PathType.Catmull)] - [TestCase(PathType.PerfectCurve)] - public void TestMultipleSegment(PathType type) + [TestCase(SplineType.Linear, null)] + [TestCase(SplineType.BSpline, null)] + [TestCase(SplineType.BSpline, 3)] + [TestCase(SplineType.Catmull, null)] + [TestCase(SplineType.PerfectCurve, null)] + public void TestMultipleSegment(SplineType splineType, int? degree) { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero)); - path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(new PathType { SplineType = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); }); } @@ -139,9 +141,9 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100, 0))); - path.ControlPoints.AddRange(createSegment(PathType.Bezier, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100))); - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.BEZIER, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero)); }); } @@ -157,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0))); }); } @@ -170,11 +172,11 @@ namespace osu.Game.Tests.Visual.Gameplay switch (points) { case 2: - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(0, 100))); break; case 4: - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); break; } }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs index 0f16d3f394..3cbd5eefac 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs @@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay { HitWindows = new HitWindows(), StartTime = t += spacing, - Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), + Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, HitSampleInfo.BANK_SOFT) }, }, }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs index 635d9f9604..e4d99f6741 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs @@ -52,59 +52,68 @@ namespace osu.Game.Tests.Visual.Gameplay { } - [TestCase(PathType.Linear)] - [TestCase(PathType.Bezier)] - [TestCase(PathType.Catmull)] - [TestCase(PathType.PerfectCurve)] - public void TestSingleSegment(PathType type) - => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + [TestCase(SplineType.Linear, null)] + [TestCase(SplineType.BSpline, null)] + [TestCase(SplineType.BSpline, 3)] + [TestCase(SplineType.Catmull, null)] + [TestCase(SplineType.PerfectCurve, null)] + public void TestSingleSegment(SplineType splineType, int? degree) + => AddStep("create path", () => path.ControlPoints.AddRange(createSegment( + new PathType { SplineType = splineType, Degree = degree }, + Vector2.Zero, + new Vector2(0, 100), + new Vector2(100), + new Vector2(0, 200), + new Vector2(200) + ))); - [TestCase(PathType.Linear)] - [TestCase(PathType.Bezier)] - [TestCase(PathType.Catmull)] - [TestCase(PathType.PerfectCurve)] - public void TestMultipleSegment(PathType type) + [TestCase(SplineType.Linear, null)] + [TestCase(SplineType.BSpline, null)] + [TestCase(SplineType.BSpline, 3)] + [TestCase(SplineType.Catmull, null)] + [TestCase(SplineType.PerfectCurve, null)] + public void TestMultipleSegment(SplineType splineType, int? degree) { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero)); - path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(new PathType { SplineType = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); }); } [Test] public void TestAddControlPoint() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100)))); AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = new Vector2(100) })); } [Test] public void TestInsertControlPoint() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100)))); AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = new Vector2(0, 100) })); } [Test] public void TestRemoveControlPoint() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("remove second point", () => path.ControlPoints.RemoveAt(1)); } [Test] public void TestChangePathType() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); - AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.Bezier); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("change type to bezier", () => path.ControlPoints[0].Type = PathType.BEZIER); } [Test] public void TestAddSegmentByChangingType() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)))); - AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.Bezier); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)))); + AddStep("change second point type to bezier", () => path.ControlPoints[1].Type = PathType.BEZIER); } [Test] @@ -112,8 +121,8 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); - path.ControlPoints[1].Type = PathType.Bezier; + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints[1].Type = PathType.BEZIER; }); AddStep("change second point type to null", () => path.ControlPoints[1].Type = null); @@ -124,8 +133,8 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); - path.ControlPoints[1].Type = PathType.Bezier; + path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints[1].Type = PathType.BEZIER; }); AddStep("remove second point", () => path.ControlPoints.RemoveAt(1)); @@ -140,11 +149,11 @@ namespace osu.Game.Tests.Visual.Gameplay switch (points) { case 2: - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(0, 100))); break; case 4: - path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); break; } }); @@ -153,35 +162,35 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestLengthenLastSegment() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 300); } [Test] public void TestShortenLastSegment() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150); } [Test] public void TestShortenFirstSegment() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("shorten first segment", () => path.ExpectedDistance.Value = 50); } [Test] public void TestShortenToZeroLength() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("shorten to 0 length", () => path.ExpectedDistance.Value = 0); } [Test] public void TestShortenToNegativeLength() { - AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10); } @@ -197,7 +206,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; double[] distances = { 100d, 200d, 300d }; - AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear)))); + AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.LINEAR)))); AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 300))); AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1))); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 4f8e935ee4..7029f61459 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -437,7 +438,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.PerfectCurve; + bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECTCURVE; // 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. @@ -455,19 +456,23 @@ namespace osu.Game.Beatmaps.Formats { switch (point.Type) { - case PathType.Bezier: + case { SplineType: SplineType.BSpline, Degree: > 0 }: + writer.Write($"B{point.Type.Value.Degree}|"); + break; + + case { SplineType: SplineType.BSpline, Degree: <= 0 }: writer.Write("B|"); break; - case PathType.Catmull: + case { SplineType: SplineType.Catmull }: writer.Write("C|"); break; - case PathType.PerfectCurve: + case { SplineType: SplineType.PerfectCurve }: writer.Write("P|"); break; - case PathType.Linear: + case { SplineType: SplineType.Linear }: writer.Write("L|"); break; } diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index ece705f685..9ca12a79dd 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -78,10 +78,10 @@ namespace osu.Game.Database // wherein the last control point of an otherwise-single-segment slider path has a different type than previous, // which would lead to sliders being mangled when exported back to stable. // normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below, - // which outputs a slider path containing only Bezier control points, + // which outputs a slider path containing only BEZIER control points, // but a non-inherited last control point is (rightly) not considered to be starting a new segment, // therefore it would fail to clear the `CountSegments() <= 1` check. - // by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier. + // by clearing explicitly we both fix the issue and avoid unnecessary conversions to BEZIER. if (hasPath.Path.ControlPoints.Count > 1) hasPath.Path.ControlPoints[^1].Type = null; diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 0c878fa1fd..74fbe7d8f9 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -68,26 +68,26 @@ namespace osu.Game.Rulesets.Objects // The current vertex ends the segment var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); - var segmentType = controlPoints[start].Type ?? PathType.Linear; + var segmentType = controlPoints[start].Type ?? PathType.LINEAR; switch (segmentType) { - case PathType.Catmull: + case { SplineType: SplineType.Catmull }: result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position); - break; - case PathType.Linear: + case { SplineType: SplineType.Linear }: result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position); - break; - case PathType.PerfectCurve: + case { SplineType: SplineType.PerfectCurve }: result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position)); - break; default: + if (segmentType.Degree != null) + throw new NotImplementedException("BSpline conversion of arbitrary degree is not implemented."); + foreach (Vector2 v in segmentVertices) { result.Add(v + position); @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Objects } /// - /// Converts a path of control points to an identical path using only Bezier type control points. + /// Converts a path of control points to an identical path using only BEZIER type control points. /// /// The control points of the path. /// The list of bezier control points. @@ -124,38 +124,38 @@ namespace osu.Game.Rulesets.Objects // The current vertex ends the segment var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); - var segmentType = controlPoints[start].Type ?? PathType.Linear; + var segmentType = controlPoints[start].Type ?? PathType.LINEAR; switch (segmentType) { - case PathType.Catmull: + case { SplineType: SplineType.Catmull }: foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices)) { for (int j = 0; j < segment.Length - 1; j++) { - result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null)); + result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } break; - case PathType.Linear: + case { SplineType: SplineType.Linear }: foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices)) { for (int j = 0; j < segment.Length - 1; j++) { - result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null)); + result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.BEZIER : null)); } } break; - case PathType.PerfectCurve: + case { SplineType: SplineType.PerfectCurve }: var circleResult = ConvertCircleToBezierAnchors(segmentVertices); for (int j = 0; j < circleResult.Length - 1; j++) { - result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.Bezier : null)); + result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.BEZIER : null)); } break; @@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Objects default: for (int j = 0; j < segmentVertices.Length - 1; j++) { - result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? PathType.Bezier : null)); + result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? segmentType : null)); } break; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index d20f2d31bb..30f4c092d9 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -224,16 +224,18 @@ namespace osu.Game.Rulesets.Objects.Legacy { default: case 'C': - return PathType.Catmull; + return new PathType(SplineType.Catmull); case 'B': - return PathType.Bezier; + if (input.Length > 1 && int.TryParse(input.Substring(1), out int degree) && degree > 0) + return new PathType { SplineType = SplineType.BSpline, Degree = degree }; + return new PathType(SplineType.BSpline); case 'L': - return PathType.Linear; + return new PathType(SplineType.Linear); case 'P': - return PathType.PerfectCurve; + return new PathType(SplineType.PerfectCurve); } } @@ -320,14 +322,14 @@ namespace osu.Game.Rulesets.Objects.Legacy readPoint(endPoint, offset, out vertices[^1]); // Edge-case rules (to match stable). - if (type == PathType.PerfectCurve) + if (type == PathType.PERFECTCURVE) { if (vertices.Length != 3) - type = PathType.Bezier; + type = PathType.BEZIER; else if (isLinear(vertices)) { // osu-stable special-cased colinear perfect curves to a linear path - type = PathType.Linear; + type = PathType.LINEAR; } } @@ -349,10 +351,10 @@ namespace osu.Game.Rulesets.Objects.Legacy if (vertices[endIndex].Position != vertices[endIndex - 1].Position) continue; - // Legacy Catmull sliders don't support multiple segments, so adjacent Catmull segments should be treated as a single one. + // Legacy CATMULL sliders don't support multiple segments, so adjacent CATMULL segments should be treated as a single one. // Importantly, this is not applied to the first control point, which may duplicate the slider path's position // resulting in a duplicate (0,0) control point in the resultant list. - if (type == PathType.Catmull && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) + if (type == PathType.CATMULL && endIndex > 1 && FormatVersion < LegacyBeatmapEncoder.FIRST_LAZER_VERSION) continue; // The last control point of each segment is not allowed to start a new implicit segment. diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 0ac057578b..4c24c111be 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; +using Microsoft.Toolkit.HighPerformance; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -260,7 +261,7 @@ namespace osu.Game.Rulesets.Objects // The current vertex ends the segment var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); - var segmentType = ControlPoints[start].Type ?? PathType.Linear; + var segmentType = ControlPoints[start].Type ?? PathType.LINEAR; // No need to calculate path when there is only 1 vertex if (segmentVertices.Length == 1) @@ -288,12 +289,12 @@ namespace osu.Game.Rulesets.Objects private List calculateSubPath(ReadOnlySpan subControlPoints, PathType type) { - switch (type) + switch (type.SplineType) { - case PathType.Linear: + case SplineType.Linear: return PathApproximator.ApproximateLinear(subControlPoints); - case PathType.PerfectCurve: + case SplineType.PerfectCurve: if (subControlPoints.Length != 3) break; @@ -305,11 +306,11 @@ namespace osu.Game.Rulesets.Objects return subPath; - case PathType.Catmull: + case SplineType.Catmull: return PathApproximator.ApproximateCatmull(subControlPoints); } - return PathApproximator.ApproximateBezier(subControlPoints); + return PathApproximator.ApproximateBSpline(subControlPoints, type.Degree ?? subControlPoints.Length); } private void calculateLength() diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 6c88f01249..d7e5e4574d 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Objects { var controlPoints = sliderPath.ControlPoints; - var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList(); + var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type is null).ToList(); // Inherited points after a linear point, as well as the first control point if it inherited, // should be treated as linear points, so their types are temporarily changed to linear. - inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear); + inheritedLinearPoints.ForEach(p => p.Type = PathType.LINEAR); double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray(); @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects inheritedLinearPoints.ForEach(p => p.Type = null); // Recalculate middle perfect curve control points at the end of the slider path. - if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && segmentEnds.Any()) + if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECTCURVE && controlPoints[^2].Type is null && segmentEnds.Any()) { double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0; double lastSegmentEnd = segmentEnds[^1]; diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index 923ce9eba4..41472fd8b5 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -1,13 +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 System; +using System.Diagnostics; + namespace osu.Game.Rulesets.Objects.Types { - public enum PathType + public enum SplineType { Catmull, - Bezier, + BSpline, Linear, PerfectCurve } + + public struct PathType + { + public static readonly PathType CATMULL = new PathType(SplineType.Catmull); + public static readonly PathType BEZIER = new PathType(SplineType.BSpline); + public static readonly PathType LINEAR = new PathType(SplineType.Linear); + public static readonly PathType PERFECTCURVE = new PathType(SplineType.PerfectCurve); + + /// + /// The type of the spline that should be used to interpret the control points of the path. + /// + public SplineType SplineType { get; init; } + + /// + /// The degree of a BSpline. Unused if is not . + /// Null means the degree is equal to the number of control points, 1 means linear, 2 means quadratic, etc. + /// + public int? Degree { get; init; } + + public PathType(SplineType splineType) + { + SplineType = splineType; + Degree = null; + } + + public override int GetHashCode() + => HashCode.Combine(SplineType, Degree); + + public override bool Equals(object? obj) + => obj is PathType pathType && this == pathType; + + public static bool operator ==(PathType a, PathType b) + => a.SplineType == b.SplineType && a.Degree == b.Degree; + + public static bool operator !=(PathType a, PathType b) + => a.SplineType != b.SplineType || a.Degree != b.Degree; + + public static PathType BSpline(int degree) + { + Debug.Assert(degree > 0); + return new PathType { SplineType = SplineType.BSpline, Degree = degree }; + } + } } diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 793d43f7ef..a114529bf9 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -246,13 +246,13 @@ namespace osu.Game.Screens.Play.HUD barPath = new SliderPath(new[] { - new PathControlPoint(new Vector2(0, 0), PathType.Linear), - new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.Bezier), + new PathControlPoint(new Vector2(0, 0), PathType.LINEAR), + new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.BEZIER), new PathControlPoint(new Vector2(curveStart, 0)), - new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.Linear), - new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.Bezier), + new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.LINEAR), + new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.BEZIER), new PathControlPoint(new Vector2(curveEnd, BarHeight.Value)), - new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.Linear), + new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.LINEAR), new PathControlPoint(new Vector2(barLength, BarHeight.Value)), }); From 3f85aa79c5b4402b714da14295b6b796ffa8d6e6 Mon Sep 17 00:00:00 2001 From: cs Date: Sat, 11 Nov 2023 10:45:22 +0100 Subject: [PATCH 683/896] Add free-hand drawing of sliders to the editor --- .../Sliders/SliderPlacementBlueprint.cs | 86 +++++++++++++++++-- .../Edit/ISliderDrawingSettingsProvider.cs | 12 +++ .../Edit/OsuHitObjectComposer.cs | 8 +- .../Edit/OsuSliderDrawingSettingsProvider.cs | 68 +++++++++++++++ osu.Game/Rulesets/Objects/SliderPath.cs | 8 +- 5 files changed, 171 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs create mode 100644 osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 8f0a2ee781..a5c6ae9465 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -44,6 +45,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } + [Resolved(CanBeNull = true)] + private ISliderDrawingSettingsProvider drawingSettingsProvider { get; set; } + + private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder(); + protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; public SliderPlacementBlueprint() @@ -73,6 +79,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { base.LoadComplete(); inputManager = GetContainingInputManager(); + + drawingSettingsProvider.Tolerance.BindValueChanged(e => + { + if (bSplineBuilder.Tolerance != e.NewValue) + bSplineBuilder.Tolerance = e.NewValue; + updateSliderPathFromBSplineBuilder(); + }, true); } [Resolved] @@ -98,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders ApplyDefaultsToHitObject(); break; - case SliderPlacementState.Body: + case SliderPlacementState.ControlPoints: updateCursor(); break; } @@ -115,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders beginCurve(); break; - case SliderPlacementState.Body: + case SliderPlacementState.ControlPoints: if (canPlaceNewControlPoint(out var lastPoint)) { // Place a new point by detatching the current cursor. @@ -139,9 +152,62 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return true; } + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button == MouseButton.Left) + { + switch (state) + { + case SliderPlacementState.Initial: + return true; + + case SliderPlacementState.ControlPoints: + if (HitObject.Path.ControlPoints.Count < 3) + { + var lastCp = HitObject.Path.ControlPoints.LastOrDefault(); + if (lastCp != cursor) + return false; + + bSplineBuilder.Clear(); + bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position); + setState(SliderPlacementState.Drawing); + return true; + } + return false; + } + } + return base.OnDragStart(e); + } + + protected override void OnDrag(DragEvent e) + { + base.OnDrag(e); + + bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position); + updateSliderPathFromBSplineBuilder(); + } + + private void updateSliderPathFromBSplineBuilder() + { + Scheduler.AddOnce(static self => + { + var cps = self.bSplineBuilder.GetControlPoints(); + self.HitObject.Path.ControlPoints.RemoveRange(1, self.HitObject.Path.ControlPoints.Count - 1); + self.HitObject.Path.ControlPoints.AddRange(cps.Select(v => new PathControlPoint(v))); + }, this); + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + + if (state == SliderPlacementState.Drawing) + endCurve(); + } + protected override void OnMouseUp(MouseUpEvent e) { - if (state == SliderPlacementState.Body && e.Button == MouseButton.Right) + if (state == SliderPlacementState.ControlPoints && e.Button == MouseButton.Right) endCurve(); base.OnMouseUp(e); } @@ -149,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void beginCurve() { BeginPlacement(commitStart: true); - setState(SliderPlacementState.Body); + setState(SliderPlacementState.ControlPoints); } private void endCurve() @@ -169,6 +235,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updatePathType() { + if (state == SliderPlacementState.Drawing) + { + segmentStart.Type = PathType.BSpline(3); + return; + } + switch (currentSegmentLength) { case 1: @@ -201,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } // Update the cursor position. - var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All); + var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All); cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; } else if (cursor != null) @@ -248,7 +320,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private enum SliderPlacementState { Initial, - Body, + ControlPoints, + Drawing, + DrawingFinalization } } } diff --git a/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs new file mode 100644 index 0000000000..1138588259 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs @@ -0,0 +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.Framework.Bindables; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public interface ISliderDrawingSettingsProvider + { + BindableFloat Tolerance { get; } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0f8c960b65..d958b558cf 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -63,6 +63,9 @@ namespace osu.Game.Rulesets.Osu.Edit [Cached(typeof(IDistanceSnapProvider))] protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); + [Cached(typeof(ISliderDrawingSettingsProvider))] + protected readonly OsuSliderDrawingSettingsProvider SliderDrawingSettingsProvider = new OsuSliderDrawingSettingsProvider(); + [BackgroundDependencyLoader] private void load() { @@ -96,8 +99,11 @@ namespace osu.Game.Rulesets.Osu.Edit RightToolbox.Add(new TransformToolboxGroup { - RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler + RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }); + + AddInternal(SliderDrawingSettingsProvider); + SliderDrawingSettingsProvider.AttachToToolbox(RightToolbox); } protected override ComposeBlueprintContainer CreateBlueprintContainer() diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs new file mode 100644 index 0000000000..ba2c39e1b5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.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 System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public partial class OsuSliderDrawingSettingsProvider : Drawable, ISliderDrawingSettingsProvider + { + public BindableFloat Tolerance { get; } = new BindableFloat(0.1f) + { + MinValue = 0.05f, + MaxValue = 1f, + Precision = 0.01f + }; + + private BindableInt sliderTolerance = new BindableInt(10) + { + MinValue = 5, + MaxValue = 100 + }; + + private ExpandableSlider toleranceSlider = null!; + + private EditorToolboxGroup? toolboxGroup; + + public OsuSliderDrawingSettingsProvider() + { + sliderTolerance.BindValueChanged(v => + { + float newValue = v.NewValue / 100f; + if (!Precision.AlmostEquals(newValue, Tolerance.Value, 1e-7f)) + Tolerance.Value = newValue; + }); + Tolerance.BindValueChanged(v => + { + int newValue = (int)Math.Round(v.NewValue * 100f); + if (sliderTolerance.Value != newValue) + sliderTolerance.Value = newValue; + }); + } + + public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer) + { + toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("drawing") + { + Children = new Drawable[] + { + toleranceSlider = new ExpandableSlider + { + Current = sliderTolerance + } + } + }); + + sliderTolerance.BindValueChanged(e => + { + toleranceSlider.ContractedLabelText = $"Tolerance: {e.NewValue:N0}"; + toleranceSlider.ExpandedLabelText = $"Tolerance: {e.NewValue:N0}"; + }, true); + } + } +} diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 4c24c111be..75f1ab868d 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -292,13 +292,13 @@ namespace osu.Game.Rulesets.Objects switch (type.SplineType) { case SplineType.Linear: - return PathApproximator.ApproximateLinear(subControlPoints); + return PathApproximator.LinearToPiecewiseLinear(subControlPoints); case SplineType.PerfectCurve: if (subControlPoints.Length != 3) break; - List subPath = PathApproximator.ApproximateCircularArc(subControlPoints); + List subPath = PathApproximator.CircularArcToPiecewiseLinear(subControlPoints); // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. if (subPath.Count == 0) @@ -307,10 +307,10 @@ namespace osu.Game.Rulesets.Objects return subPath; case SplineType.Catmull: - return PathApproximator.ApproximateCatmull(subControlPoints); + return PathApproximator.CatmullToPiecewiseLinear(subControlPoints); } - return PathApproximator.ApproximateBSpline(subControlPoints, type.Degree ?? subControlPoints.Length); + return PathApproximator.BSplineToPiecewiseLinear(subControlPoints, type.Degree ?? subControlPoints.Length); } private void calculateLength() From 4e1e19728cb8ff9e11f18ab9c0e8635c2cc2ba9a Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 11 Nov 2023 14:02:42 +0100 Subject: [PATCH 684/896] Refactor HitObject selection in Composer --- .../Edit/ManiaHitObjectComposer.cs | 43 ++++++++++++++----- .../Edit/OsuHitObjectComposer.cs | 36 +++++++++++++--- .../Rulesets/Edit/EditorTimestampParser.cs | 40 +---------------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 14 ++---- osu.Game/Screens/Edit/Editor.cs | 11 +---- 5 files changed, 68 insertions(+), 76 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index d217f04651..b04d3f895d 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -3,16 +3,15 @@ #nullable disable -using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit.Compose.Components; @@ -50,22 +49,44 @@ namespace osu.Game.Rulesets.Mania.Edit new HoldNoteCompositionTool() }; + private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$"); + public override string ConvertSelectionToString() - => string.Join(ObjectSeparator, EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); + => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); - public override bool HandleHitObjectSelection(HitObject hitObject, string objectInfo) + public override void SelectHitObjects(double timestamp, string objectDescription) { - if (hitObject is not ManiaHitObject maniaHitObject) - return false; + if (!selection_regex.IsMatch(objectDescription)) + return; - double[] split = objectInfo.Split('|').Select(double.Parse).ToArray(); + List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); + string[] split = objectDescription.Split(',').ToArray(); + + for (int i = 0; i < split.Length; i++) + { + ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => shouldBeSelected(h, split[i])); + + if (current == null) + continue; + + EditorBeatmap.SelectedHitObjects.Add(current); + + if (i < split.Length - 1) + remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); + } + } + + private bool shouldBeSelected(ManiaHitObject hitObject, string objectInfo) + { + string[] split = objectInfo.Split('|').ToArray(); if (split.Length != 2) return false; - double timeValue = split[0]; - double columnValue = split[1]; - return Math.Abs(maniaHitObject.StartTime - timeValue) < 0.5 - && Math.Abs(maniaHitObject.Column - columnValue) < 0.5; + if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column)) + return false; + + return hitObject.StartTime == time + && hitObject.Column == column; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0c63cf71d8..e9c222b0e7 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -103,18 +104,39 @@ namespace osu.Game.Rulesets.Osu.Edit protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(this); + private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$"); + public override string ConvertSelectionToString() - => string.Join(ObjectSeparator, selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); - public override bool HandleHitObjectSelection(HitObject hitObject, string objectInfo) + public override void SelectHitObjects(double timestamp, string objectDescription) { - if (hitObject is not OsuHitObject osuHitObject) + if (!selection_regex.IsMatch(objectDescription)) + return; + + List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); + string[] split = objectDescription.Split(',').ToArray(); + + for (int i = 0; i < split.Length; i++) + { + OsuHitObject current = remainingHitObjects.FirstOrDefault(h => shouldBeSelected(h, split[i])); + + if (current == null) + continue; + + EditorBeatmap.SelectedHitObjects.Add(current); + + if (i < split.Length - 1) + remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); + } + } + + private bool shouldBeSelected(OsuHitObject hitObject, string objectInfo) + { + if (!int.TryParse(objectInfo, out int combo) || combo < 1) return false; - if (!int.TryParse(objectInfo, out int comboValue) || comboValue < 1) - return false; - - return osuHitObject.IndexInCurrentCombo + 1 == comboValue; + return hitObject.IndexInCurrentCombo + 1 == combo; } private DistanceSnapGrid distanceSnapGrid; diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index 4e5a696102..7d4247b269 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.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 System.Text.RegularExpressions; -using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Edit { @@ -31,43 +29,7 @@ namespace osu.Game.Rulesets.Edit Debug.Assert(times.Length == 3); - return (times[0] * 60 + times[1]) * 1_000 + times[2]; - } - - public static List GetSelectedHitObjects(HitObjectComposer composer, IReadOnlyList editorHitObjects, string objectsGroup, double position) - { - List hitObjects = editorHitObjects.Where(x => x.StartTime >= position).ToList(); - List selectedObjects = new List(); - - string[] objectsToSelect = objectsGroup.Split(composer.ObjectSeparator).ToArray(); - - foreach (string objectInfo in objectsToSelect) - { - HitObject? current = hitObjects.FirstOrDefault(x => composer.HandleHitObjectSelection(x, objectInfo)); - - if (current == null) - continue; - - selectedObjects.Add(current); - hitObjects = hitObjects.Where(x => x != current && x.StartTime >= current.StartTime).ToList(); - } - - // Stable behavior - // - always selects the next closest object when `objectsGroup` only has one (combo) item - if (objectsToSelect.Length != 1 || objectsGroup.Contains('|')) - return selectedObjects; - - HitObject? nextClosest = editorHitObjects.FirstOrDefault(x => x.StartTime >= position); - if (nextClosest == null) - return selectedObjects; - - if (nextClosest.StartTime <= (selectedObjects.FirstOrDefault()?.StartTime ?? position)) - { - selectedObjects.Clear(); - selectedObjects.Add(nextClosest); - } - - return selectedObjects; + return (times[0] * 60 + times[1]) * 1000 + times[2]; } } } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index f6cddcc0d2..52a525e84f 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -529,17 +529,11 @@ namespace osu.Game.Rulesets.Edit public virtual string ConvertSelectionToString() => string.Empty; /// - /// The custom logic that decides whether a HitObject should be selected when clicking an editor timestamp link + /// Each ruleset has it's own selection method /// - /// The hitObject being checked - /// A single hitObject's information created with - /// Whether a HitObject should be selected or not - public virtual bool HandleHitObjectSelection(HitObject hitObject, string objectInfo) => false; - - /// - /// A character that separates the selection in - /// - public virtual char ObjectSeparator => ','; + /// The given timestamp + /// The selected object information between the brackets + public virtual void SelectHitObjects(double timestamp, string objectDescription) { } #region IPositionSnapProvider diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 58c3ae809c..03d3e3a1f8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1194,15 +1194,8 @@ namespace osu.Game.Screens.Edit if (Mode.Value != EditorScreenMode.Compose) Mode.Value = EditorScreenMode.Compose; - List selected = EditorTimestampParser.GetSelectedHitObjects( - currentScreen.Dependencies.Get(), - editorBeatmap.HitObjects.ToList(), - objectsGroup, - position - ); - - if (selected.Any()) - editorBeatmap.SelectedHitObjects.AddRange(selected); + // Let the Ruleset handle selection + currentScreen.Dependencies.Get().SelectHitObjects(position, objectsGroup); } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From 6ddecfd8062d6b1b0d62a6064d7d0b0b2c8d4760 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 11 Nov 2023 14:13:06 +0100 Subject: [PATCH 685/896] Update Test cases --- .../TestSceneOpenEditorTimestampInMania.cs | 9 ++---- .../TestSceneOpenEditorTimestampInOsu.cs | 32 +++++-------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs index 6ec5dcee4c..3c6a9f3b42 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs @@ -84,17 +84,14 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor [Test] public void TestUnusualSelection() { - addStepClickLink("00:00:000 (0|1)", "invalid link"); + addStepClickLink("00:00:000 (0|1)", "wrong offset"); AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); addReset(); - addStepClickLink("00:00:000 (0)", "std link"); - AddAssert("snap and select 1", () => checkSnapAndSelectColumn(2_170, new List<(int, int)> - { (2_170, 2) }) - ); + addStepClickLink("00:00:000 (2)", "std link"); + AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); addReset(); - // TODO: discuss - this selects the first 2 objects on Stable, do we want that or is this fine? addStepClickLink("00:00:000 (1,2)", "std link"); AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs index d69f482d29..93573b5ad8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs @@ -71,40 +71,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { HitObject firstObject = null!; - addStepClickLink("00:00:000 (1,2,3)", "invalid offset"); - AddAssert("snap to next, select 1-2-3", () => + addStepClickLink("00:00:000 (0)", "invalid combo"); + AddAssert("snap to next, select none", () => { firstObject = EditorBeatmap.HitObjects.First(); - return checkSnapAndSelectCombo(firstObject.StartTime, 1, 2, 3); + return checkSnapAndSelectCombo(firstObject.StartTime); }); addReset(); - addStepClickLink("00:00:956 (2,3,4)", "invalid offset"); + addStepClickLink("00:00:000 (1)", "wrong offset"); + AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); + + addReset(); + addStepClickLink("00:00:956 (2,3,4)", "wrong offset"); AddAssert("snap to next, select 2-3-4", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3, 4)); - addReset(); - addStepClickLink("00:00:000 (0)", "invalid offset"); - AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - - addReset(); - addStepClickLink("00:00:000 (1)", "invalid offset"); - AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - - addReset(); - addStepClickLink("00:00:000 (2)", "invalid offset"); - AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); - - addReset(); - addStepClickLink("00:00:000 (2,3)", "invalid offset"); - AddAssert("snap to 1, select 2-3", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3)); - addReset(); addStepClickLink("00:00:956 (956|1,956|2)", "mania link"); AddAssert("snap to next, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); - - addReset(); - addStepClickLink("00:00:000 (0|1)", "mania link"); - AddAssert("snap to 1, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); } } } From 54b8244a18ad3e74d40962be20cab996ba63e90d Mon Sep 17 00:00:00 2001 From: cs Date: Sat, 11 Nov 2023 15:02:06 +0100 Subject: [PATCH 686/896] CI Fixup --- .../Sliders/Components/PathControlPointPiece.cs | 8 ++++---- .../Components/PathControlPointVisualiser.cs | 2 +- .../Sliders/SliderPlacementBlueprint.cs | 2 ++ .../Edit/OsuSliderDrawingSettingsProvider.cs | 10 ++++------ .../Visual/Gameplay/TestSceneBezierConverter.cs | 4 ++-- .../Visual/Gameplay/TestSceneSliderPath.cs | 4 ++-- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 11 +++++------ osu.Game/Rulesets/Objects/BezierConverter.cs | 12 ++++++------ .../Objects/Legacy/ConvertHitObjectParser.cs | 3 ++- osu.Game/Rulesets/Objects/SliderPath.cs | 3 +-- osu.Game/Rulesets/Objects/Types/PathType.cs | 16 ++++++++-------- 11 files changed, 37 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 9658e5f6c3..53228cff82 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -261,17 +261,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (pathType) { - case { SplineType: SplineType.Catmull }: + case { Type: SplineType.Catmull }: return colours.SeaFoam; - case { SplineType: SplineType.BSpline, Degree: null }: + case { Type: SplineType.BSpline, Degree: null }: return colours.PinkLighter; - case { SplineType: SplineType.BSpline, Degree: >= 1 }: + case { Type: SplineType.BSpline, Degree: >= 1 }: int idx = Math.Clamp(pathType.Degree.Value, 0, 3); return new[] { colours.PinkDarker, colours.PinkDark, colours.Pink, colours.PinkLight }[idx]; - case { SplineType: SplineType.PerfectCurve }: + case { Type: SplineType.PerfectCurve }: return colours.PurpleDark; default: diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index b5c9016538..4e85835652 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); - if (type.HasValue && type.Value.SplineType == SplineType.PerfectCurve) + if (type.HasValue && type.Value.Type == SplineType.PerfectCurve) { // Can't always create a circular arc out of 4 or more points, // so we split the segment into one 3-point circular arc segment diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index a5c6ae9465..20f11c3585 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -173,9 +173,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders setState(SliderPlacementState.Drawing); return true; } + return false; } } + return base.OnDragStart(e); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs index ba2c39e1b5..ae772f53fc 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 0.01f }; - private BindableInt sliderTolerance = new BindableInt(10) + private readonly BindableInt sliderTolerance = new BindableInt(10) { MinValue = 5, MaxValue = 100 @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Osu.Edit private ExpandableSlider toleranceSlider = null!; - private EditorToolboxGroup? toolboxGroup; - public OsuSliderDrawingSettingsProvider() { sliderTolerance.BindValueChanged(v => @@ -47,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Edit public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer) { - toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("drawing") + toolboxContainer.Add(new EditorToolboxGroup("drawing") { Children = new Drawable[] { @@ -60,8 +58,8 @@ namespace osu.Game.Rulesets.Osu.Edit sliderTolerance.BindValueChanged(e => { - toleranceSlider.ContractedLabelText = $"Tolerance: {e.NewValue:N0}"; - toleranceSlider.ExpandedLabelText = $"Tolerance: {e.NewValue:N0}"; + toleranceSlider.ContractedLabelText = $"C. P. S.: {e.NewValue:N0}"; + toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {e.NewValue:N0}"; }, true); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs index 5eb82ccbdc..e2333011c7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(SplineType.Catmull, null)] [TestCase(SplineType.PerfectCurve, null)] public void TestSingleSegment(SplineType splineType, int? degree) - => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(new PathType { SplineType = splineType, Degree = degree }, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); + => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, Vector2.Zero, new Vector2(0, 100), new Vector2(100)))); [TestCase(SplineType.Linear, null)] [TestCase(SplineType.BSpline, null)] @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("create path", () => { path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero)); - path.ControlPoints.AddRange(createSegment(new PathType { SplineType = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs index e4d99f6741..d44af45fe4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(SplineType.PerfectCurve, null)] public void TestSingleSegment(SplineType splineType, int? degree) => AddStep("create path", () => path.ControlPoints.AddRange(createSegment( - new PathType { SplineType = splineType, Degree = degree }, + new PathType { Type = splineType, Degree = degree }, Vector2.Zero, new Vector2(0, 100), new Vector2(100), @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("create path", () => { path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero)); - path.ControlPoints.AddRange(createSegment(new PathType { SplineType = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(new PathType { Type = splineType, Degree = degree }, new Vector2(0, 100), new Vector2(100), Vector2.Zero)); }); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7029f61459..ff446206ac 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -456,23 +455,23 @@ namespace osu.Game.Beatmaps.Formats { switch (point.Type) { - case { SplineType: SplineType.BSpline, Degree: > 0 }: + case { Type: SplineType.BSpline, Degree: > 0 }: writer.Write($"B{point.Type.Value.Degree}|"); break; - case { SplineType: SplineType.BSpline, Degree: <= 0 }: + case { Type: SplineType.BSpline, Degree: <= 0 }: writer.Write("B|"); break; - case { SplineType: SplineType.Catmull }: + case { Type: SplineType.Catmull }: writer.Write("C|"); break; - case { SplineType: SplineType.PerfectCurve }: + case { Type: SplineType.PerfectCurve }: writer.Write("P|"); break; - case { SplineType: SplineType.Linear }: + case { Type: SplineType.Linear }: writer.Write("L|"); break; } diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 74fbe7d8f9..ed86fc10e0 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -72,15 +72,15 @@ namespace osu.Game.Rulesets.Objects switch (segmentType) { - case { SplineType: SplineType.Catmull }: + case { Type: SplineType.Catmull }: result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position); break; - case { SplineType: SplineType.Linear }: + case { Type: SplineType.Linear }: result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position); break; - case { SplineType: SplineType.PerfectCurve }: + case { Type: SplineType.PerfectCurve }: result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position)); break; @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Objects switch (segmentType) { - case { SplineType: SplineType.Catmull }: + case { Type: SplineType.Catmull }: foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices)) { for (int j = 0; j < segment.Length - 1; j++) @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Objects break; - case { SplineType: SplineType.Linear }: + case { Type: SplineType.Linear }: foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices)) { for (int j = 0; j < segment.Length - 1; j++) @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Objects break; - case { SplineType: SplineType.PerfectCurve }: + case { Type: SplineType.PerfectCurve }: var circleResult = ConvertCircleToBezierAnchors(segmentVertices); for (int j = 0; j < circleResult.Length - 1; j++) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 30f4c092d9..6a13a897c4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -228,7 +228,8 @@ namespace osu.Game.Rulesets.Objects.Legacy case 'B': if (input.Length > 1 && int.TryParse(input.Substring(1), out int degree) && degree > 0) - return new PathType { SplineType = SplineType.BSpline, Degree = degree }; + return new PathType { Type = SplineType.BSpline, Degree = degree }; + return new PathType(SplineType.BSpline); case 'L': diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index 75f1ab868d..e9a192669f 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using Microsoft.Toolkit.HighPerformance; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Caching; @@ -289,7 +288,7 @@ namespace osu.Game.Rulesets.Objects private List calculateSubPath(ReadOnlySpan subControlPoints, PathType type) { - switch (type.SplineType) + switch (type.Type) { case SplineType.Linear: return PathApproximator.LinearToPiecewiseLinear(subControlPoints); diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index 41472fd8b5..a6e8e173d4 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Objects.Types PerfectCurve } - public struct PathType + public readonly struct PathType { public static readonly PathType CATMULL = new PathType(SplineType.Catmull); public static readonly PathType BEZIER = new PathType(SplineType.BSpline); @@ -24,36 +24,36 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The type of the spline that should be used to interpret the control points of the path. /// - public SplineType SplineType { get; init; } + public SplineType Type { get; init; } /// - /// The degree of a BSpline. Unused if is not . + /// The degree of a BSpline. Unused if is not . /// Null means the degree is equal to the number of control points, 1 means linear, 2 means quadratic, etc. /// public int? Degree { get; init; } public PathType(SplineType splineType) { - SplineType = splineType; + Type = splineType; Degree = null; } public override int GetHashCode() - => HashCode.Combine(SplineType, Degree); + => HashCode.Combine(Type, Degree); public override bool Equals(object? obj) => obj is PathType pathType && this == pathType; public static bool operator ==(PathType a, PathType b) - => a.SplineType == b.SplineType && a.Degree == b.Degree; + => a.Type == b.Type && a.Degree == b.Degree; public static bool operator !=(PathType a, PathType b) - => a.SplineType != b.SplineType || a.Degree != b.Degree; + => a.Type != b.Type || a.Degree != b.Degree; public static PathType BSpline(int degree) { Debug.Assert(degree > 0); - return new PathType { SplineType = SplineType.BSpline, Degree = degree }; + return new PathType { Type = SplineType.BSpline, Degree = degree }; } } } From c367697559dc8981ce601e44afa45a3ce46a6dc9 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sat, 11 Nov 2023 16:14:26 +0100 Subject: [PATCH 687/896] Changed TIME_REGEX to accept everything in the parentheses - changed osu-web link to the current value --- osu.Game/Rulesets/Edit/EditorTimestampParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index 7d4247b269..b1488d298f 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Edit public static class EditorTimestampParser { // 00:00:000 (1,2,3) - test - // regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10 - public static readonly Regex TIME_REGEX = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)"); + // osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 + public static readonly Regex TIME_REGEX = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\([^)]+\))?)"); public static string[] GetRegexGroups(string timestamp) { From 3e8c89e282c74874a8c6885228910d70b64d2e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 12 Nov 2023 07:42:41 +0900 Subject: [PATCH 688/896] Fix one more reference to removed setting --- .../Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index e9092bba1b..15a7b48323 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(HealthProcessor))] private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); - protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), BarLength = { Value = 1f } }; + protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 1f }; protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) }; protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) }; From 798e677c092a4ad02e0fe0a81163c14b57ff90d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Nov 2023 15:12:04 +0900 Subject: [PATCH 689/896] Refactor `KeyCounterDisplay` to use autosize A previous attempt at this was unsuccessful due to a partially off-screen elements not getting the correct size early enough (see https://github.com/ppy/osu/issues/14793). This can be accounted for by setting `AlwaysPresent` when visibility is expected. This fixes [test failures](https://github.com/ppy/osu/actions/runs/6838444698/job/18595535795) due to the newly added `Width` / `Height` being persisted with floating-point errors (by not persisting the values in the first place, via `AutoSize.Both`). --- osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs | 12 ------------ .../Play/HUD/DefaultKeyCounterDisplay.cs | 14 -------------- osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs | 17 ++++++++++++++++- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs index 984c2a7287..44b90fcad0 100644 --- a/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs @@ -10,8 +10,6 @@ namespace osu.Game.Screens.Play { public partial class ArgonKeyCounterDisplay : KeyCounterDisplay { - private const int duration = 100; - protected override FillFlowContainer KeyFlow { get; } public ArgonKeyCounterDisplay() @@ -25,16 +23,6 @@ namespace osu.Game.Screens.Play }; } - protected override void Update() - { - base.Update(); - - Size = KeyFlow.Size; - } - protected override KeyCounter CreateCounter(InputTrigger trigger) => new ArgonKeyCounter(trigger); - - protected override void UpdateVisibility() - => KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); } } diff --git a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs index e459574243..e0f96d32bc 100644 --- a/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultKeyCounterDisplay.cs @@ -10,7 +10,6 @@ namespace osu.Game.Screens.Play.HUD { public partial class DefaultKeyCounterDisplay : KeyCounterDisplay { - private const int duration = 100; private const double key_fade_time = 80; protected override FillFlowContainer KeyFlow { get; } @@ -25,15 +24,6 @@ namespace osu.Game.Screens.Play.HUD }; } - protected override void Update() - { - base.Update(); - - // Don't use autosize as it will shrink to zero when KeyFlow is hidden. - // In turn this can cause the display to be masked off screen and never become visible again. - Size = KeyFlow.Size; - } - protected override KeyCounter CreateCounter(InputTrigger trigger) => new DefaultKeyCounter(trigger) { FadeTime = key_fade_time, @@ -41,10 +31,6 @@ namespace osu.Game.Screens.Play.HUD KeyUpTextColor = KeyUpTextColor, }; - protected override void UpdateVisibility() => - // Isolate changing visibility of the key counters from fading this component. - KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration); - private Color4 keyDownTextColor = Color4.DarkGray; public Color4 KeyDownTextColor diff --git a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs index e7e866932e..0a5d6b763e 100644 --- a/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/HUD/KeyCounterDisplay.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Rulesets.UI; @@ -31,13 +32,27 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private InputCountController controller { get; set; } = null!; - protected abstract void UpdateVisibility(); + private const int duration = 100; + + protected void UpdateVisibility() + { + bool visible = AlwaysVisible.Value || ConfigVisibility.Value; + + // Isolate changing visibility of the key counters from fading this component. + KeyFlow.FadeTo(visible ? 1 : 0, duration); + + // Ensure a valid size is immediately obtained even if partially off-screen + // See https://github.com/ppy/osu/issues/14793. + KeyFlow.AlwaysPresent = visible; + } protected abstract KeyCounter CreateCounter(InputTrigger trigger); [BackgroundDependencyLoader] private void load(OsuConfigManager config, DrawableRuleset? drawableRuleset) { + AutoSizeAxes = Axes.Both; + config.BindWith(OsuSetting.KeyOverlay, ConfigVisibility); if (drawableRuleset != null) From 31feeb5ddc616bb437c052b2b1680070c9d72563 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Nov 2023 17:21:17 +0900 Subject: [PATCH 690/896] Disable new rider EAP inspection in test class --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 93005271a9..d0a45856b2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -203,6 +203,7 @@ namespace osu.Game.Tests.Visual.Ranking public IBeatmap Beatmap { get; } + // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized public TestBeatmapConverter(IBeatmap beatmap) { Beatmap = beatmap; From 469b9e2546a0eea247f19bdc9e793170fe7c7b05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Nov 2023 17:28:15 +0900 Subject: [PATCH 691/896] Increase size and adjust positioning of max combo display in argon skin --- osu.Game/Skinning/ArgonSkin.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 715d5e4600..637467c748 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -182,15 +182,14 @@ namespace osu.Game.Skinning if (songProgress != null) { const float padding = 10; + // Hard to find this at runtime, so taken from the most expanded state during replay. + const float song_progress_offset_height = 36 + padding; songProgress.Position = new Vector2(0, -padding); songProgress.Scale = new Vector2(0.9f, 1); if (keyCounter != null && hitError != null) { - // Hard to find this at runtime, so taken from the most expanded state during replay. - const float song_progress_offset_height = 36 + padding; - keyCounter.Anchor = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight; keyCounter.Position = new Vector2(-(hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); @@ -200,7 +199,7 @@ namespace osu.Game.Skinning { combo.Anchor = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft; - combo.Position = new Vector2(hitError.Width + padding, -50); + combo.Position = new Vector2((hitError.Width + padding), -(padding * 2 + song_progress_offset_height)); } } } @@ -221,7 +220,10 @@ namespace osu.Game.Skinning new ArgonHealthDisplay(), new BoxElement(), new ArgonAccuracyCounter(), - new ArgonComboCounter(), + new ArgonComboCounter + { + Scale = new Vector2(1.3f) + }, new BarHitErrorMeter(), new BarHitErrorMeter(), new ArgonSongProgress(), From 4e7c40f1d7392376d68989ad5fa26e53430c55c6 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sun, 12 Nov 2023 14:58:46 +0100 Subject: [PATCH 692/896] Do Split and Parse before checking HitObjects --- .../Edit/ManiaHitObjectComposer.cs | 28 ++++++++----------- .../Edit/OsuHitObjectComposer.cs | 19 +++++-------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index b04d3f895d..d580f2d025 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -60,33 +60,27 @@ namespace osu.Game.Rulesets.Mania.Edit return; List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); - string[] split = objectDescription.Split(',').ToArray(); + string[] splitDescription = objectDescription.Split(',').ToArray(); - for (int i = 0; i < split.Length; i++) + for (int i = 0; i < splitDescription.Length; i++) { - ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => shouldBeSelected(h, split[i])); + string[] split = splitDescription[i].Split('|').ToArray(); + if (split.Length != 2) + continue; + + if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column)) + continue; + + ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column); if (current == null) continue; EditorBeatmap.SelectedHitObjects.Add(current); - if (i < split.Length - 1) + if (i < splitDescription.Length - 1) remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); } } - - private bool shouldBeSelected(ManiaHitObject hitObject, string objectInfo) - { - string[] split = objectInfo.Split('|').ToArray(); - if (split.Length != 2) - return false; - - if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column)) - return false; - - return hitObject.StartTime == time - && hitObject.Column == column; - } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e9c222b0e7..1dfa02fe83 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -115,30 +115,25 @@ namespace osu.Game.Rulesets.Osu.Edit return; List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); - string[] split = objectDescription.Split(',').ToArray(); + string[] splitDescription = objectDescription.Split(',').ToArray(); - for (int i = 0; i < split.Length; i++) + for (int i = 0; i < splitDescription.Length; i++) { - OsuHitObject current = remainingHitObjects.FirstOrDefault(h => shouldBeSelected(h, split[i])); + if (!int.TryParse(splitDescription[i], out int combo) || combo < 1) + continue; + + OsuHitObject current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo); if (current == null) continue; EditorBeatmap.SelectedHitObjects.Add(current); - if (i < split.Length - 1) + if (i < splitDescription.Length - 1) remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); } } - private bool shouldBeSelected(OsuHitObject hitObject, string objectInfo) - { - if (!int.TryParse(objectInfo, out int combo) || combo < 1) - return false; - - return hitObject.IndexInCurrentCombo + 1 == combo; - } - private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; From fab6fc9adb633d82d88afe4c76b1313946032ab4 Mon Sep 17 00:00:00 2001 From: ratinfx Date: Sun, 12 Nov 2023 15:09:15 +0100 Subject: [PATCH 693/896] Updated comments, renamed method --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 1 + osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 1 + osu.Game/OsuGame.cs | 4 ++-- osu.Game/Rulesets/Edit/EditorTimestampParser.cs | 6 ++++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index d580f2d025..92ecea812c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Mania.Edit new HoldNoteCompositionTool() }; + // 123|0,456|1,789|2 ... private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$"); public override string ConvertSelectionToString() diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 1dfa02fe83..2afbd83ce5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -104,6 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(this); + // 1,2,3,4 ... private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$"); public override string ConvertSelectionToString() diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index cde8ee1457..4e37481ba7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -434,7 +434,7 @@ namespace osu.Game break; case LinkAction.OpenEditorTimestamp: - SeekToTimestamp(argString); + HandleTimestamp(argString); break; case LinkAction.JoinMultiplayerMatch: @@ -558,7 +558,7 @@ namespace osu.Game /// Seek to a given timestamp in the Editor and select relevant HitObjects if needed /// /// The timestamp and the selected objects - public void SeekToTimestamp(string timestamp) + public void HandleTimestamp(string timestamp) { if (ScreenStack.CurrentScreen is not Editor editor) { diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index b1488d298f..e36822cc63 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -10,16 +10,18 @@ namespace osu.Game.Rulesets.Edit { public static class EditorTimestampParser { - // 00:00:000 (1,2,3) - test - // osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 + // 00:00:000 (...) - test + // original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 public static readonly Regex TIME_REGEX = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\([^)]+\))?)"); public static string[] GetRegexGroups(string timestamp) { Match match = TIME_REGEX.Match(timestamp); + string[] result = match.Success ? match.Groups.Values.Where(x => x is not Match && !x.Value.Contains(':')).Select(x => x.Value).ToArray() : Array.Empty(); + return result; } From d123ba5bcef07e6893cb8c2d633e5c35b987bccf Mon Sep 17 00:00:00 2001 From: Poyo Date: Sun, 12 Nov 2023 11:13:38 -0800 Subject: [PATCH 694/896] Fix broken tests --- .../NonVisual/Ranking/UnstableRateTest.cs | 8 +++--- .../Gameplay/TestSceneUnstableRateCounter.cs | 4 ++- ...estSceneHitEventTimingDistributionGraph.cs | 28 +++++++++---------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index 27c8270f0f..bde3c37af7 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.NonVisual.Ranking public void TestDistributedHits() { var events = Enumerable.Range(-5, 11) - .Select(t => new HitEvent(t - 5, HitResult.Great, new HitObject(), null, null)); + .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); var unstableRate = new UnstableRate(events); @@ -33,9 +33,9 @@ namespace osu.Game.Tests.NonVisual.Ranking { var events = new[] { - new HitEvent(-100, HitResult.Miss, new HitObject(), null, null), - new HitEvent(0, HitResult.Great, new HitObject(), null, null), - new HitEvent(200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), + new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null), + new HitEvent(0, 1.0, HitResult.Great, new HitObject(), null, null), + new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), }; var unstableRate = new UnstableRate(events); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs index d0e516ed39..f6c819b329 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs @@ -56,6 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay scoreProcessor.RevertResult( new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement()) { + GameplayRate = 1.0, TimeOffset = 25, Type = HitResult.Perfect, }); @@ -92,7 +93,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void applyJudgement(double offsetMs, bool alt) + private void applyJudgement(double offsetMs, bool alt, double gameplayRate = 1.0) { double placement = offsetMs; @@ -105,6 +106,7 @@ namespace osu.Game.Tests.Visual.Gameplay scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement()) { TimeOffset = placement, + GameplayRate = gameplayRate, Type = HitResult.Perfect, }); } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index a40cb41e2c..325a535731 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAroundCentre() { - createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); + createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -57,12 +57,12 @@ namespace osu.Game.Tests.Visual.Ranking { createTest(new List { - new HitEvent(-7, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(-6, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(-5, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(5, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(6, HitResult.Perfect, placeholder_object, placeholder_object, null), - new HitEvent(7, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(-5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(5, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(6, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), + new HitEvent(7, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null), }); } @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Ranking : offset > 16 ? HitResult.Good : offset > 8 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); }).ToList()); } @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Ranking : offset > 8 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); }); var narrow = CreateDistributedHitEvents(0, 50).Select(h => { @@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.Ranking : offset > 10 ? HitResult.Good : offset > 5 ? HitResult.Great : HitResult.Perfect; - return new HitEvent(h.TimeOffset, result, placeholder_object, placeholder_object, null); + return new HitEvent(h.TimeOffset, 1.0, result, placeholder_object, placeholder_object, null); }); createTest(wide.Concat(narrow).ToList()); } @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestZeroTimeOffset() { - createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); + createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList()); } [Test] @@ -129,9 +129,9 @@ namespace osu.Game.Tests.Visual.Ranking createTest(Enumerable.Range(0, 100).Select(i => { if (i % 2 == 0) - return new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null); + return new HitEvent(0, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null); - return new HitEvent(30, HitResult.Miss, placeholder_object, placeholder_object, null); + return new HitEvent(30, 1.0, HitResult.Miss, placeholder_object, placeholder_object, null); }).ToList()); } @@ -162,7 +162,7 @@ namespace osu.Game.Tests.Visual.Ranking int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10; for (int j = 0; j < count; j++) - hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null)); + hitEvents.Add(new HitEvent(centre + i - range, 1.0, HitResult.Perfect, placeholder_object, placeholder_object, null)); } return hitEvents; From 8c36604e58e84265d623f0ec19e3eec85370ef4d Mon Sep 17 00:00:00 2001 From: Poyo Date: Sun, 12 Nov 2023 11:16:06 -0800 Subject: [PATCH 695/896] Add rate-change UR tests --- .../NonVisual/Ranking/UnstableRateTest.cs | 32 +++++++++++++++++++ .../Gameplay/TestSceneUnstableRateCounter.cs | 22 +++++++++++++ 2 files changed, 54 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index bde3c37af7..5a416d05d7 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -42,5 +42,37 @@ namespace osu.Game.Tests.NonVisual.Ranking Assert.AreEqual(0, unstableRate.Value); } + + [Test] + public void TestStaticRateChange() + { + var events = new[] + { + new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(-150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(150, 1.5, HitResult.Great, new HitObject(), null, null), + }; + + var unstableRate = new UnstableRate(events); + + Assert.AreEqual(10 * 100, unstableRate.Value); + } + + [Test] + public void TestDynamicRateChange() + { + var events = new[] + { + new HitEvent(-50, 0.5, HitResult.Great, new HitObject(), null, null), + new HitEvent(75, 0.75, HitResult.Great, new HitObject(), null, null), + new HitEvent(-100, 1.0, HitResult.Great, new HitObject(), null, null), + new HitEvent(125, 1.25, HitResult.Great, new HitObject(), null, null), + }; + + var unstableRate = new UnstableRate(events); + + Assert.AreEqual(10 * 100, unstableRate.Value); + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs index f6c819b329..73ec6ea335 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneUnstableRateCounter.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -81,6 +82,27 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("UR = 250", () => counter.Current.Value == 250.0); } + [Test] + public void TestStaticRateChange() + { + AddStep("Create Display", recreateDisplay); + + AddRepeatStep("Set UR to 250 at 1.5x", () => applyJudgement(25, true, 1.5), 4); + + AddUntilStep("UR = 250/1.5", () => counter.Current.Value == Math.Round(250.0 / 1.5)); + } + + [Test] + public void TestDynamicRateChange() + { + AddStep("Create Display", recreateDisplay); + + AddRepeatStep("Set UR to 100 at 1.0x", () => applyJudgement(10, true, 1.0), 4); + AddRepeatStep("Bring UR to 100 at 1.5x", () => applyJudgement(15, true, 1.5), 4); + + AddUntilStep("UR = 100", () => counter.Current.Value == 100.0); + } + private void recreateDisplay() { Clear(); From e67725f5d6c9b6334c3e01a5717b06cd507a1f63 Mon Sep 17 00:00:00 2001 From: Poyo Date: Sun, 12 Nov 2023 12:14:19 -0800 Subject: [PATCH 696/896] Use IGameplayClock for rate --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0843fd5bdc..4ae59dccb1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -154,6 +154,9 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } + [Resolved] + private IGameplayClock gameplayClock { get; set; } = null!; + /// /// Whether the initialization logic in has applied. /// @@ -704,7 +707,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Result.RawTime = Time.Current; - Result.GameplayRate = Clock.Rate; + Result.GameplayRate = gameplayClock.GetTrueGameplayRate(); if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); From f794d4dc8384e0bba90d5063e5285a063adabcf8 Mon Sep 17 00:00:00 2001 From: Poyo Date: Sun, 12 Nov 2023 13:29:40 -0800 Subject: [PATCH 697/896] Allow gameplayClock to be null --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 4ae59dccb1..7cfb6aedf0 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -154,8 +154,8 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } - [Resolved] - private IGameplayClock gameplayClock { get; set; } = null!; + [Resolved(CanBeNull = true)] + private IGameplayClock gameplayClock { get; set; } /// /// Whether the initialization logic in has applied. @@ -707,7 +707,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Result.RawTime = Time.Current; - Result.GameplayRate = gameplayClock.GetTrueGameplayRate(); + Result.GameplayRate = gameplayClock?.GetTrueGameplayRate() ?? 1.0; if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); From c24b3543ab271536b2b98c3c4777a174bc0621c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Mon, 13 Nov 2023 12:47:12 +0900 Subject: [PATCH 698/896] Fix OnDragStart on macOS --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 20f11c3585..fe01b1b6d2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (HitObject.Path.ControlPoints.Count < 3) { var lastCp = HitObject.Path.ControlPoints.LastOrDefault(); - if (lastCp != cursor) + if (lastCp != cursor && HitObject.Path.ControlPoints.Count == 2) return false; bSplineBuilder.Clear(); From e6b4dfba369ee790ee0cee427aa702a935ce341b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Mon, 13 Nov 2023 12:49:59 +0900 Subject: [PATCH 699/896] Fix doubled control point at beginning of drawn slider --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index fe01b1b6d2..9ac28fe82a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { var cps = self.bSplineBuilder.GetControlPoints(); self.HitObject.Path.ControlPoints.RemoveRange(1, self.HitObject.Path.ControlPoints.Count - 1); - self.HitObject.Path.ControlPoints.AddRange(cps.Select(v => new PathControlPoint(v))); + self.HitObject.Path.ControlPoints.AddRange(cps.Skip(1).Select(v => new PathControlPoint(v))); }, this); } From 5fd55e55c08130773e72805ce7785b6da69031b6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Nov 2023 12:59:36 +0900 Subject: [PATCH 700/896] Add flag for combo end bonus to legacy processor --- osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs index 103569ffc3..e6db76e3b6 100644 --- a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Scoring private const double hp_slider_repeat = 4; private const double hp_slider_tick = 3; + public bool ComboEndBonus { get; set; } + private double lowestHpEver; private double lowestHpEnd; private double lowestHpComboEnd; @@ -129,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break; } - if (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo) + if (ComboEndBonus && (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo)) { increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); From 929570656d7a25fdea59141b1385a5a207b1bfc4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Nov 2023 13:12:46 +0900 Subject: [PATCH 701/896] Disallow legacy health processor from being used for gameplay --- .../Scoring/LegacyOsuHealthProcessor.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs index e6db76e3b6..aa9312500b 100644 --- a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs @@ -5,13 +5,17 @@ using System; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { - // Reference implementation for osu!stable's HP drain. + /// + /// Reference implementation for osu!stable's HP drain. + /// Cannot be used for gameplay. + /// public partial class LegacyOsuHealthProcessor : LegacyDrainingHealthProcessor { private const double hp_bar_maximum = 200; @@ -20,8 +24,6 @@ namespace osu.Game.Rulesets.Osu.Scoring private const double hp_slider_repeat = 4; private const double hp_slider_tick = 3; - public bool ComboEndBonus { get; set; } - private double lowestHpEver; private double lowestHpEnd; private double lowestHpComboEnd; @@ -44,6 +46,18 @@ namespace osu.Game.Rulesets.Osu.Scoring base.ApplyBeatmap(beatmap); } + protected override void ApplyResultInternal(JudgementResult result) + { + if (!IsSimulating) + throw new NotSupportedException("The legacy osu! health processor is not supported for gameplay."); + } + + protected override void RevertResultInternal(JudgementResult result) + { + if (!IsSimulating) + throw new NotSupportedException("The legacy osu! health processor is not supported for gameplay."); + } + protected override void Reset(bool storeResults) { hpMultiplierNormal = 1; @@ -131,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break; } - if (ComboEndBonus && (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo)) + if (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo) { increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); From 7713da499f6cb08b81cfcaf571ad79fb36f20752 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Nov 2023 13:12:56 +0900 Subject: [PATCH 702/896] Make osu ruleset use the new health processor --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 607b83d379..aaf0ab41a0 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Osu public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new OsuHealthProcessor(drainStartTime); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); From 98e6b7744bf58db8db7e348a4935fe9c5fe06e78 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Nov 2023 13:46:47 +0900 Subject: [PATCH 703/896] Cleanup --- .../Scoring/LegacyOsuHealthProcessor.cs | 11 +-- .../Scoring/OsuHealthProcessor.cs | 12 +-- .../Scoring/DrainingHealthProcessor.cs | 54 ++++++----- .../Scoring/LegacyDrainingHealthProcessor.cs | 90 ------------------- 4 files changed, 47 insertions(+), 120 deletions(-) delete mode 100644 osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs diff --git a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs index aa9312500b..f918868715 100644 --- a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Scoring /// Reference implementation for osu!stable's HP drain. /// Cannot be used for gameplay. /// - public partial class LegacyOsuHealthProcessor : LegacyDrainingHealthProcessor + public partial class LegacyOsuHealthProcessor : DrainingHealthProcessor { private const double hp_bar_maximum = 200; private const double hp_combo_geki = 14; @@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Scoring private const double hp_slider_repeat = 4; private const double hp_slider_tick = 3; + public Action? OnIterationFail; + public Action? OnIterationSuccess; + private double lowestHpEver; private double lowestHpEnd; private double lowestHpComboEnd; @@ -187,13 +190,11 @@ namespace osu.Game.Rulesets.Osu.Scoring if (fail) { - if (Log) - Console.WriteLine($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); + OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); continue; } - if (Log) - Console.WriteLine($"PASSED drop {testDrop / hp_bar_maximum}"); + OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}"); return testDrop / hp_bar_maximum; } while (true); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 2266cf9d33..3a096fca88 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; @@ -12,8 +13,11 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { - public partial class OsuHealthProcessor : LegacyDrainingHealthProcessor + public partial class OsuHealthProcessor : DrainingHealthProcessor { + public Action? OnIterationFail; + public Action? OnIterationSuccess; + private double lowestHpEver; private double lowestHpEnd; private double hpRecoveryAvailable; @@ -141,13 +145,11 @@ namespace osu.Game.Rulesets.Osu.Scoring if (fail) { - if (Log) - Console.WriteLine($"FAILED drop {testDrop}: {failReason}"); + OnIterationFail?.Invoke($"FAILED drop {testDrop}: {failReason}"); continue; } - if (Log) - Console.WriteLine($"PASSED drop {testDrop}"); + OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); return testDrop; } while (true); diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 2f81aa735e..a8808d08e5 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -41,15 +41,29 @@ namespace osu.Game.Rulesets.Scoring ///
private const double max_health_target = 0.4; - private IBeatmap beatmap; - private double gameplayEndTime; + /// + /// The drain rate as a proportion of the total health drained per millisecond. + /// + public double DrainRate { get; private set; } = 1; - private readonly double drainStartTime; - private readonly double drainLenience; + /// + /// The beatmap. + /// + protected IBeatmap Beatmap { get; private set; } + + /// + /// The time at which health starts draining. + /// + protected readonly double DrainStartTime; + + /// + /// An amount of lenience to apply to the drain rate. + /// + protected readonly double DrainLenience; private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>(); + private double gameplayEndTime; private double targetMinimumHealth; - private double drainRate = 1; private PeriodTracker noDrainPeriodTracker; @@ -63,8 +77,8 @@ namespace osu.Game.Rulesets.Scoring /// A value of 1 completely removes drain. public DrainingHealthProcessor(double drainStartTime, double drainLenience = 0) { - this.drainStartTime = drainStartTime; - this.drainLenience = Math.Clamp(drainLenience, 0, 1); + DrainStartTime = drainStartTime; + DrainLenience = Math.Clamp(drainLenience, 0, 1); } protected override void Update() @@ -75,16 +89,16 @@ namespace osu.Game.Rulesets.Scoring return; // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time - double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime); - double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime); + double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, DrainStartTime, gameplayEndTime); + double currentGameplayTime = Math.Clamp(Time.Current, DrainStartTime, gameplayEndTime); - if (drainLenience < 1) - Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime); + if (DrainLenience < 1) + Health.Value -= DrainRate * (currentGameplayTime - lastGameplayTime); } public override void ApplyBeatmap(IBeatmap beatmap) { - this.beatmap = beatmap; + Beatmap = beatmap; if (beatmap.HitObjects.Count > 0) gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); @@ -105,7 +119,7 @@ namespace osu.Game.Rulesets.Scoring targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target); // Add back a portion of the amount of HP to be drained, depending on the lenience requested. - targetMinimumHealth += drainLenience * (1 - targetMinimumHealth); + targetMinimumHealth += DrainLenience * (1 - targetMinimumHealth); // Ensure the target HP is within an acceptable range. targetMinimumHealth = Math.Clamp(targetMinimumHealth, 0, 1); @@ -125,15 +139,15 @@ namespace osu.Game.Rulesets.Scoring { base.Reset(storeResults); - drainRate = 1; + DrainRate = 1; if (storeResults) - drainRate = computeDrainRate(); + DrainRate = ComputeDrainRate(); healthIncreases.Clear(); } - private double computeDrainRate() + protected virtual double ComputeDrainRate() { if (healthIncreases.Count <= 1) return 0; @@ -152,17 +166,17 @@ namespace osu.Game.Rulesets.Scoring for (int i = 0; i < healthIncreases.Count; i++) { double currentTime = healthIncreases[i].time; - double lastTime = i > 0 ? healthIncreases[i - 1].time : drainStartTime; + double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime; // Subtract any break time from the duration since the last object - if (beatmap.Breaks.Count > 0) + if (Beatmap.Breaks.Count > 0) { // Advance the last break occuring before the current time - while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) + while (currentBreak + 1 < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak + 1].EndTime < currentTime) currentBreak++; if (currentBreak >= 0) - lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime); + lastTime = Math.Max(lastTime, Beatmap.Breaks[currentBreak].EndTime); } // Apply health adjustments diff --git a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs deleted file mode 100644 index 5d2426e4b7..0000000000 --- a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs +++ /dev/null @@ -1,90 +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; -using System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; -using osu.Game.Utils; - -namespace osu.Game.Rulesets.Scoring -{ - /// - /// A which continuously drains health.
- /// At HP=0, the minimum health reached for a perfect play is 95%.
- /// At HP=5, the minimum health reached for a perfect play is 70%.
- /// At HP=10, the minimum health reached for a perfect play is 30%. - ///
- public abstract partial class LegacyDrainingHealthProcessor : HealthProcessor - { - protected double DrainStartTime { get; } - protected double GameplayEndTime { get; private set; } - - protected IBeatmap Beatmap { get; private set; } - protected PeriodTracker NoDrainPeriodTracker { get; private set; } - - public bool Log { get; set; } - - public double DrainRate { get; private set; } - - /// - /// Creates a new . - /// - /// The time after which draining should begin. - protected LegacyDrainingHealthProcessor(double drainStartTime) - { - DrainStartTime = drainStartTime; - } - - protected override void Update() - { - base.Update(); - - if (NoDrainPeriodTracker?.IsInAny(Time.Current) == true) - return; - - // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time - double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, DrainStartTime, GameplayEndTime); - double currentGameplayTime = Math.Clamp(Time.Current, DrainStartTime, GameplayEndTime); - - Health.Value -= DrainRate * (currentGameplayTime - lastGameplayTime); - } - - public override void ApplyBeatmap(IBeatmap beatmap) - { - Beatmap = beatmap; - - if (beatmap.HitObjects.Count > 0) - GameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); - - NoDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period( - beatmap.HitObjects - .Select(hitObject => hitObject.GetEndTime()) - .Where(endTime => endTime <= breakPeriod.StartTime) - .DefaultIfEmpty(double.MinValue) - .Last(), - beatmap.HitObjects - .Select(hitObject => hitObject.StartTime) - .Where(startTime => startTime >= breakPeriod.EndTime) - .DefaultIfEmpty(double.MaxValue) - .First() - ))); - - base.ApplyBeatmap(beatmap); - } - - protected override void Reset(bool storeResults) - { - base.Reset(storeResults); - - DrainRate = 1; - - if (storeResults) - DrainRate = ComputeDrainRate(); - } - - protected abstract double ComputeDrainRate(); - } -} From 8ad8764947835e9ce4070338b6e1ea81016df2b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Nov 2023 14:06:18 +0900 Subject: [PATCH 704/896] 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 2870696c03..1f6a65c450 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 f1159f58b9..70525a5c59 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 65b41138a347718297f71509e6a1dbc30d468543 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Nov 2023 14:06:24 +0900 Subject: [PATCH 705/896] Add option to disable combo end --- osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs index f918868715..e92c3c9b97 100644 --- a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Scoring public Action? OnIterationFail; public Action? OnIterationSuccess; + public bool ApplyComboEndBonus { get; set; } = true; private double lowestHpEver; private double lowestHpEnd; @@ -148,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break; } - if (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo) + if (ApplyComboEndBonus && (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo)) { increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); From 35d4c483d78399fa3322b7a1db79110da36a759f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Nov 2023 14:06:34 +0900 Subject: [PATCH 706/896] Improve commenting around small tick/large tick health --- osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 3a096fca88..5802f8fc0d 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -185,12 +185,12 @@ namespace osu.Game.Rulesets.Osu.Scoring return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2); case HitResult.SmallTickHit: - // This result is always as a result of the slider tail. + // This result always comes from the slider tail, which is judged the same as a repeat. increase = 0.02; break; case HitResult.LargeTickHit: - // This result is either a result of a slider tick or a repeat. + // This result comes from either a slider tick or repeat. increase = hitObject is SliderTick ? 0.015 : 0.02; break; From fa976a5aa0eeb94dccbcd58b00d51a1c9d7e52df Mon Sep 17 00:00:00 2001 From: cs Date: Mon, 13 Nov 2023 08:24:09 +0100 Subject: [PATCH 707/896] Fix code style/quality issues --- .../TestSceneJuiceStreamSelectionBlueprint.cs | 6 +++--- .../JuiceStreamPathTest.cs | 2 +- .../Checks/CheckOffscreenObjectsTest.cs | 2 +- .../TestSceneOsuEditorSelectInvalidPath.cs | 2 +- .../TestScenePathControlPointVisualiser.cs | 8 ++++---- .../TestSceneSliderControlPointPiece.cs | 18 ++++++++--------- .../Editor/TestSceneSliderLengthValidity.cs | 2 +- .../TestSceneSliderPlacementBlueprint.cs | 16 +++++++-------- .../Editor/TestSceneSliderReversal.cs | 2 +- .../Editor/TestSceneSliderSnapping.cs | 10 +++++----- .../Editor/TestSceneSliderSplitting.cs | 20 +++++++++---------- .../TestSceneSlider.cs | 6 +++--- .../TestSceneSliderInput.cs | 2 +- .../TestSceneSliderSnaking.cs | 6 +++--- .../Components/PathControlPointPiece.cs | 14 ++++++------- .../Components/PathControlPointVisualiser.cs | 2 +- .../Sliders/SliderPlacementBlueprint.cs | 2 +- .../Edit/OsuSliderDrawingSettingsProvider.cs | 8 +++++--- .../Formats/LegacyBeatmapDecoderTest.cs | 8 ++++---- .../Gameplay/TestSceneBezierConverter.cs | 8 ++++---- .../Visual/Gameplay/TestSceneSliderPath.cs | 4 ++-- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 18 +++++++---------- .../Edit/ComposerDistanceSnapProvider.cs | 2 +- osu.Game/Rulesets/Edit/IToolboxAttachment.cs | 10 ++++++++++ osu.Game/Rulesets/Objects/BezierConverter.cs | 8 ++++---- .../Objects/Legacy/ConvertHitObjectParser.cs | 10 +++++----- .../Rulesets/Objects/SliderPathExtensions.cs | 2 +- osu.Game/Rulesets/Objects/Types/PathType.cs | 12 +++++++---- osu.Game/osu.Game.csproj | 2 +- 29 files changed, 112 insertions(+), 100 deletions(-) create mode 100644 osu.Game/Rulesets/Edit/IToolboxAttachment.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 16b51d414a..c96f32d87c 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor AddStep("update hit object path", () => { - hitObject.Path = new SliderPath(PathType.PERFECTCURVE, new[] + hitObject.Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(100, 100), @@ -190,14 +190,14 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor [Test] public void TestVertexResampling() { - addBlueprintStep(100, 100, new SliderPath(PathType.PERFECTCURVE, new[] + addBlueprintStep(100, 100, new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(100, 100), new Vector2(50, 200), }), 0.5); AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count); - AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); + AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); addAddVertexSteps(150, 150); AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.LINEAR); } diff --git a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs index 82f24633b5..9fb55fc057 100644 --- a/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/JuiceStreamPathTest.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests } while (rng.Next(2) != 0); int length = sliderPath.ControlPoints.Count - start + 1; - sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.LINEAR : length == 3 ? PathType.PERFECTCURVE : PathType.BEZIER; + sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.LINEAR : length == 3 ? PathType.PERFECT_CURVE : PathType.BEZIER; } while (rng.Next(3) != 0); if (rng.Next(5) == 0) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs index 8612a8eb57..5db6dc6cdd 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckOffscreenObjectsTest.cs @@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Path = new SliderPath(new[] { // Circular arc shoots over the top of the screen. - new PathControlPoint(new Vector2(0, 0), PathType.PERFECTCURVE), + new PathControlPoint(new Vector2(0, 0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(-100, -200)), new PathControlPoint(new Vector2(100, -200)) }), diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs index 7ea4d40b90..43dae38004 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorSelectInvalidPath.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.PERFECTCURVE), + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(-100, 0)), new PathControlPoint(new Vector2(100, 20)) }; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 16800997f4..811ecf53e9 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addContextMenuItemStep("Perfect curve"); assertControlPointPathType(0, PathType.BEZIER); - assertControlPointPathType(1, PathType.PERFECTCURVE); + assertControlPointPathType(1, PathType.PERFECT_CURVE); assertControlPointPathType(3, PathType.BEZIER); } @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addContextMenuItemStep("Perfect curve"); assertControlPointPathType(0, PathType.BEZIER); - assertControlPointPathType(2, PathType.PERFECTCURVE); + assertControlPointPathType(2, PathType.PERFECT_CURVE); assertControlPointPathType(4, null); } @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addContextMenuItemStep("Perfect curve"); assertControlPointPathType(0, PathType.LINEAR); - assertControlPointPathType(1, PathType.PERFECTCURVE); + assertControlPointPathType(1, PathType.PERFECT_CURVE); assertControlPointPathType(3, PathType.LINEAR); } @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor createVisualiser(true); addControlPointStep(new Vector2(200), PathType.BEZIER); - addControlPointStep(new Vector2(300), PathType.PERFECTCURVE); + addControlPointStep(new Vector2(300), PathType.PERFECT_CURVE); addControlPointStep(new Vector2(500, 300)); addControlPointStep(new Vector2(700, 200), PathType.BEZIER); addControlPointStep(new Vector2(500, 100)); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs index 1d8d2cf01a..99ced30ffe 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderControlPointPiece.cs @@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(256, 192), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PERFECTCURVE), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(1, new Vector2(150, 50)); - assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("three control point pieces selected", () => this.ChildrenOfType>().Count(piece => piece.IsSelected.Value) == 3); assertControlPointPosition(2, new Vector2(450, 50)); - assertControlPointType(2, PathType.PERFECTCURVE); + assertControlPointType(2, PathType.PERFECT_CURVE); assertControlPointPosition(3, new Vector2(550, 50)); @@ -249,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider moved", () => Precision.AlmostEquals(slider.Position, new Vector2(256, 192) + new Vector2(150, 50))); assertControlPointPosition(0, Vector2.Zero); - assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); assertControlPointPosition(1, new Vector2(0, 100)); @@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(1, new Vector2(150, 50)); - assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); assertControlPointPosition(4, new Vector2(150, 150)); - assertControlPointType(2, PathType.PERFECTCURVE); + assertControlPointType(2, PathType.PERFECT_CURVE); } [Test] @@ -312,12 +312,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.BEZIER); AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10)))); - AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PERFECTCURVE); + AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PERFECT_CURVE); moveMouseToControlPoint(4); AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); - assertControlPointType(3, PathType.PERFECTCURVE); + assertControlPointType(3, PathType.PERFECT_CURVE); addMovementStep(new Vector2(350, 0.01f)); AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left)); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs index 38ebeb7e8f..931c8c9e63 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderLengthValidity.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor PathControlPoint[] points = { - new PathControlPoint(new Vector2(0), PathType.PERFECTCURVE), + new PathControlPoint(new Vector2(0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(100, 0)), new PathControlPoint(new Vector2(0, 10)) }; diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 4b120c1a3f..ecfc8105f1 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); assertControlPointPosition(1, new Vector2(100, 0)); - assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPosition(1, new Vector2(100, 0)); assertControlPointPosition(2, new Vector2(100)); assertControlPointType(0, PathType.LINEAR); - assertControlPointType(1, PathType.PERFECTCURVE); + assertControlPointType(1, PathType.PERFECT_CURVE); } [Test] @@ -269,8 +269,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPosition(2, new Vector2(100)); assertControlPointPosition(3, new Vector2(200, 100)); assertControlPointPosition(4, new Vector2(200)); - assertControlPointType(0, PathType.PERFECTCURVE); - assertControlPointType(2, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); + assertControlPointType(2, PathType.PERFECT_CURVE); } [Test] @@ -326,7 +326,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -347,7 +347,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); } [Test] @@ -385,7 +385,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertControlPointCount(3); - assertControlPointType(0, PathType.PERFECTCURVE); + assertControlPointType(0, PathType.PERFECT_CURVE); } private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position))); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs index 0ddfc40946..a44c16a2e0 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private readonly PathControlPoint[][] paths = { createPathSegment( - PathType.PERFECTCURVE, + PathType.PERFECT_CURVE, new Vector2(200, -50), new Vector2(250, 0) ), diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs index c984d9168e..541fefb3a6 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSnapping.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { ControlPoints = { - new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(136, 205)), new PathControlPoint(new Vector2(-4, 226)) } @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { OsuSelectionHandler selectionHandler; - AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); + AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("rotate 90 degrees ccw", () => @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor selectionHandler.HandleRotation(-90); }); - AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); + AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); } [Test] @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { OsuSelectionHandler selectionHandler; - AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); + AddAssert("first control point perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); AddStep("flip slider horizontally", () => @@ -232,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor selectionHandler.OnPressed(new KeyBindingPressEvent(InputManager.CurrentState, GlobalAction.EditorFlipVertically)); }); - AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECTCURVE); + AddAssert("first control point still perfect", () => slider.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE); } [Test] diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs index cded9165f4..6c7733e68a 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSplitting.cs @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(0, 50), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PERFECTCURVE), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) @@ -73,20 +73,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 2 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, - (new Vector2(0, 50), PathType.PERFECTCURVE), + (new Vector2(0, 50), PathType.PERFECT_CURVE), (new Vector2(150, 200), null), (new Vector2(300, 50), null) ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], slider.StartTime, endTime + split_gap, - (new Vector2(300, 50), PathType.PERFECTCURVE), + (new Vector2(300, 50), PathType.PERFECT_CURVE), (new Vector2(400, 50), null), (new Vector2(400, 200), null) )); AddStep("undo", () => Editor.Undo()); AddAssert("original slider restored", () => EditorBeatmap.HitObjects.Count == 1 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, endTime, - (new Vector2(0, 50), PathType.PERFECTCURVE), + (new Vector2(0, 50), PathType.PERFECT_CURVE), (new Vector2(150, 200), null), - (new Vector2(300, 50), PathType.PERFECTCURVE), + (new Vector2(300, 50), PathType.PERFECT_CURVE), (new Vector2(400, 50), null), (new Vector2(400, 200), null) )); @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(0, 50), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(150, 150)), new PathControlPoint(new Vector2(300, 0), PathType.BEZIER), new PathControlPoint(new Vector2(400, 0)), @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddAssert("slider split", () => slider is not null && EditorBeatmap.HitObjects.Count == 3 && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[0], 0, EditorBeatmap.HitObjects[1].StartTime - split_gap, - (new Vector2(0, 50), PathType.PERFECTCURVE), + (new Vector2(0, 50), PathType.PERFECT_CURVE), (new Vector2(150, 200), null), (new Vector2(300, 50), null) ) && sliderCreatedFor((Slider)EditorBeatmap.HitObjects[1], EditorBeatmap.HitObjects[0].GetEndTime() + split_gap, slider.StartTime - split_gap, @@ -165,9 +165,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Position = new Vector2(0, 50), Path = new SliderPath(new[] { - new PathControlPoint(Vector2.Zero, PathType.PERFECTCURVE), + new PathControlPoint(Vector2.Zero, PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(150, 150)), - new PathControlPoint(new Vector2(300, 0), PathType.PERFECTCURVE), + new PathControlPoint(new Vector2(300, 0), PathType.PERFECT_CURVE), new PathControlPoint(new Vector2(400, 0)), new PathControlPoint(new Vector2(400, 150)) }) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 60003e7950..4600db8174 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(239, 176), - Path = new SliderPath(PathType.PERFECTCURVE, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(154, 28), @@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Tests SliderVelocityMultiplier = speedMultiplier, StartTime = Time.Current + time_offset, Position = new Vector2(0, -(distance / 2)), - Path = new SliderPath(PathType.PERFECTCURVE, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(0, distance), @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + time_offset, Position = new Vector2(-max_length / 2, 0), - Path = new SliderPath(PathType.PERFECTCURVE, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(max_length / 2, max_length / 2), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 08836ef819..0f7dd8b7bc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -478,7 +478,7 @@ namespace osu.Game.Rulesets.Osu.Tests StartTime = time_slider_start, Position = new Vector2(0, 0), SliderVelocityMultiplier = 0.1f, - Path = new SliderPath(PathType.PERFECTCURVE, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(slider_path_length, 0), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index ebc5143aed..912b2b0626 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 3000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PERFECTCURVE, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(300, 200) @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 13000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PERFECTCURVE, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(300, 200) @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = 23000, Position = new Vector2(100, 100), - Path = new SliderPath(PathType.PERFECTCURVE, new[] + Path = new SliderPath(PathType.PERFECT_CURVE, new[] { Vector2.Zero, new Vector2(300, 200) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index 53228cff82..ac9048d5c7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components ///
private void updatePathType() { - if (ControlPoint.Type != PathType.PERFECTCURVE) + if (ControlPoint.Type != PathType.PERFECT_CURVE) return; if (PointsInSegment.Count > 3) @@ -259,19 +259,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (ControlPoint.Type is not PathType pathType) return colours.Yellow; - switch (pathType) + switch (pathType.Type) { - case { Type: SplineType.Catmull }: + case SplineType.Catmull: return colours.SeaFoam; - case { Type: SplineType.BSpline, Degree: null }: - return colours.PinkLighter; + case SplineType.BSpline: + if (!pathType.Degree.HasValue) + return colours.PinkLighter; - case { Type: SplineType.BSpline, Degree: >= 1 }: int idx = Math.Clamp(pathType.Degree.Value, 0, 3); return new[] { colours.PinkDarker, colours.PinkDark, colours.Pink, colours.PinkLight }[idx]; - case { Type: SplineType.PerfectCurve }: + case SplineType.PerfectCurve: return colours.PurpleDark; default: diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 4e85835652..5ab050ed48 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -368,7 +368,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // todo: hide/disable items which aren't valid for selected points curveTypeItems.Add(createMenuItemForPathType(PathType.LINEAR)); - curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECTCURVE)); + curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE)); curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER)); curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3))); curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 9ac28fe82a..c722f0fdbc 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -251,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders break; case 3: - segmentStart.Type = PathType.PERFECTCURVE; + segmentStart.Type = PathType.PERFECT_CURVE; break; default: diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs index ae772f53fc..643732d90a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Rulesets.Osu.Edit { - public partial class OsuSliderDrawingSettingsProvider : Drawable, ISliderDrawingSettingsProvider + public partial class OsuSliderDrawingSettingsProvider : Drawable, ISliderDrawingSettingsProvider, IToolboxAttachment { public BindableFloat Tolerance { get; } = new BindableFloat(0.1f) { @@ -27,12 +27,14 @@ namespace osu.Game.Rulesets.Osu.Edit private ExpandableSlider toleranceSlider = null!; - public OsuSliderDrawingSettingsProvider() + protected override void LoadComplete() { + base.LoadComplete(); + sliderTolerance.BindValueChanged(v => { float newValue = v.NewValue / 100f; - if (!Precision.AlmostEquals(newValue, Tolerance.Value, 1e-7f)) + if (!Precision.AlmostEquals(newValue, Tolerance.Value)) Tolerance.Value = newValue; }); Tolerance.BindValueChanged(v => diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 18c21046fb..34b96bbd3f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -808,7 +808,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var first = ((IHasPath)decoded.HitObjects[0]).Path; Assert.That(first.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECTCURVE)); + Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECT_CURVE)); Assert.That(first.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244))); Assert.That(first.ControlPoints[1].Type, Is.EqualTo(null)); @@ -827,7 +827,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var second = ((IHasPath)decoded.HitObjects[1]).Path; Assert.That(second.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECTCURVE)); + Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PERFECT_CURVE)); Assert.That(second.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244))); Assert.That(second.ControlPoints[1].Type, Is.EqualTo(null)); Assert.That(second.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3))); @@ -904,12 +904,12 @@ namespace osu.Game.Tests.Beatmaps.Formats var seventh = ((IHasPath)decoded.HitObjects[6]).Path; Assert.That(seventh.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero)); - Assert.That(seventh.ControlPoints[0].Type == PathType.PERFECTCURVE); + Assert.That(seventh.ControlPoints[0].Type == PathType.PERFECT_CURVE); Assert.That(seventh.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145))); Assert.That(seventh.ControlPoints[1].Type == null); Assert.That(seventh.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75))); - Assert.That(seventh.ControlPoints[2].Type == PathType.PERFECTCURVE); + Assert.That(seventh.ControlPoints[2].Type == PathType.PERFECT_CURVE); Assert.That(seventh.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145))); Assert.That(seventh.ControlPoints[3].Type == null); Assert.That(seventh.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20))); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs index e2333011c7..27497f77be 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.Gameplay { path.ControlPoints.AddRange(createSegment(PathType.LINEAR, Vector2.Zero, new Vector2(100, 0))); path.ControlPoints.AddRange(createSegment(PathType.BEZIER, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100))); - path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero)); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero)); }); } @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create path", () => { - path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0))); }); } @@ -172,11 +172,11 @@ namespace osu.Game.Tests.Visual.Gameplay switch (points) { case 2: - path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(0, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100))); break; case 4: - path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); break; } }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs index d44af45fe4..44a2e5fb9b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs @@ -149,11 +149,11 @@ namespace osu.Game.Tests.Visual.Gameplay switch (points) { case 2: - path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(0, 100))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100))); break; case 4: - path.ControlPoints.AddRange(createSegment(PathType.PERFECTCURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); + path.ControlPoints.AddRange(createSegment(PathType.PERFECT_CURVE, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))); break; } }); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index ff446206ac..b375a6f7ff 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -437,7 +437,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.PERFECTCURVE; + bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE; // 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. @@ -453,25 +453,21 @@ namespace osu.Game.Beatmaps.Formats if (needsExplicitSegment) { - switch (point.Type) + switch (point.Type?.Type) { - case { Type: SplineType.BSpline, Degree: > 0 }: - writer.Write($"B{point.Type.Value.Degree}|"); + case SplineType.BSpline: + writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|"); break; - case { Type: SplineType.BSpline, Degree: <= 0 }: - writer.Write("B|"); - break; - - case { Type: SplineType.Catmull }: + case SplineType.Catmull: writer.Write("C|"); break; - case { Type: SplineType.PerfectCurve }: + case SplineType.PerfectCurve: writer.Write("P|"); break; - case { Type: SplineType.Linear }: + case SplineType.Linear: writer.Write("L|"); break; } diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index ddf539771d..68411d2b01 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -29,7 +29,7 @@ using osu.Game.Screens.Edit.Components.TernaryButtons; namespace osu.Game.Rulesets.Edit { - public abstract partial class ComposerDistanceSnapProvider : Component, IDistanceSnapProvider, IScrollBindingHandler + public abstract partial class ComposerDistanceSnapProvider : Component, IDistanceSnapProvider, IScrollBindingHandler, IToolboxAttachment { private const float adjust_step = 0.1f; diff --git a/osu.Game/Rulesets/Edit/IToolboxAttachment.cs b/osu.Game/Rulesets/Edit/IToolboxAttachment.cs new file mode 100644 index 0000000000..7d7c5980b2 --- /dev/null +++ b/osu.Game/Rulesets/Edit/IToolboxAttachment.cs @@ -0,0 +1,10 @@ +// 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.Rulesets.Edit +{ + public interface IToolboxAttachment + { + void AttachToToolbox(ExpandingToolboxContainer toolbox); + } +} diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index ed86fc10e0..5dc0839d37 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -126,9 +126,9 @@ namespace osu.Game.Rulesets.Objects var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = controlPoints[start].Type ?? PathType.LINEAR; - switch (segmentType) + switch (segmentType.Type) { - case { Type: SplineType.Catmull }: + case SplineType.Catmull: foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices)) { for (int j = 0; j < segment.Length - 1; j++) @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Objects break; - case { Type: SplineType.Linear }: + case SplineType.Linear: foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices)) { for (int j = 0; j < segment.Length - 1; j++) @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Objects break; - case { Type: SplineType.PerfectCurve }: + case SplineType.PerfectCurve: var circleResult = ConvertCircleToBezierAnchors(segmentVertices); for (int j = 0; j < circleResult.Length - 1; j++) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 6a13a897c4..92a92dca8f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -224,19 +224,19 @@ namespace osu.Game.Rulesets.Objects.Legacy { default: case 'C': - return new PathType(SplineType.Catmull); + return PathType.CATMULL; case 'B': if (input.Length > 1 && int.TryParse(input.Substring(1), out int degree) && degree > 0) - return new PathType { Type = SplineType.BSpline, Degree = degree }; + return PathType.BSpline(degree); return new PathType(SplineType.BSpline); case 'L': - return new PathType(SplineType.Linear); + return PathType.LINEAR; case 'P': - return new PathType(SplineType.PerfectCurve); + return PathType.PERFECT_CURVE; } } @@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.Objects.Legacy readPoint(endPoint, offset, out vertices[^1]); // Edge-case rules (to match stable). - if (type == PathType.PERFECTCURVE) + if (type == PathType.PERFECT_CURVE) { if (vertices.Length != 3) type = PathType.BEZIER; diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index d7e5e4574d..29b34ae4f0 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects inheritedLinearPoints.ForEach(p => p.Type = null); // Recalculate middle perfect curve control points at the end of the slider path. - if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECTCURVE && controlPoints[^2].Type is null && segmentEnds.Any()) + if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type is null && segmentEnds.Any()) { double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0; double lastSegmentEnd = segmentEnds[^1]; diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index a6e8e173d4..4fb48bb8b4 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; namespace osu.Game.Rulesets.Objects.Types { @@ -14,12 +13,12 @@ namespace osu.Game.Rulesets.Objects.Types PerfectCurve } - public readonly struct PathType + public readonly struct PathType : IEquatable { public static readonly PathType CATMULL = new PathType(SplineType.Catmull); public static readonly PathType BEZIER = new PathType(SplineType.BSpline); public static readonly PathType LINEAR = new PathType(SplineType.Linear); - public static readonly PathType PERFECTCURVE = new PathType(SplineType.PerfectCurve); + public static readonly PathType PERFECT_CURVE = new PathType(SplineType.PerfectCurve); /// /// The type of the spline that should be used to interpret the control points of the path. @@ -52,8 +51,13 @@ namespace osu.Game.Rulesets.Objects.Types public static PathType BSpline(int degree) { - Debug.Assert(degree > 0); + if (degree <= 0) + throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero."); + return new PathType { Type = SplineType.BSpline, Degree = degree }; } + + public bool Equals(PathType other) + => Type == other.Type && Degree == other.Degree; } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fc10f90e8f..8d42cd22e3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From e3137d575bbea46423c8124a9ade25fab18d03f2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Nov 2023 01:11:17 +0900 Subject: [PATCH 708/896] Fix osu! and base HP processor break time implementation --- .../TestSceneOsuHealthProcessor.cs | 94 +++++++++++++++++++ .../Scoring/OsuHealthProcessor.cs | 25 ++--- .../TestSceneDrainingHealthProcessor.cs | 81 +++++++++++++++- .../Scoring/DrainingHealthProcessor.cs | 44 ++++----- 4 files changed, 203 insertions(+), 41 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs new file mode 100644 index 0000000000..f810bbf155 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs @@ -0,0 +1,94 @@ +// 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.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestSceneOsuHealthProcessor + { + [Test] + public void TestNoBreak() + { + OsuHealthProcessor hp = new OsuHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(1.4E-5).Within(0.1E-5)); + } + + [Test] + public void TestSingleBreak() + { + OsuHealthProcessor hp = new OsuHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1500) + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + + [Test] + public void TestOverlappingBreak() + { + OsuHealthProcessor hp = new OsuHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1400), + new BreakPeriod(750, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + + [Test] + public void TestSequentialBreak() + { + OsuHealthProcessor hp = new OsuHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1000), + new BreakPeriod(1000, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.3E-5).Within(0.1E-5)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 5802f8fc0d..3207c7fb51 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -3,9 +3,7 @@ using System; using System.Linq; -using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; @@ -64,25 +62,16 @@ namespace osu.Game.Rulesets.Osu.Scoring { HitObject h = Beatmap.HitObjects[i]; - // Find active break (between current and lastTime) - double localLastTime = lastTime; - double breakTime = 0; - - // TODO: This doesn't handle overlapping/sequential breaks correctly (/b/614). - // Subtract any break time from the duration since the last object - if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) { - BreakPeriod e = Beatmap.Breaks[currentBreak]; - - if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) - { - // consider break start equal to object end time for version 8+ since drain stops during this time - breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; - currentBreak++; - } + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = h.StartTime; + currentBreak++; } - reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + reduceHp(testDrop * (h.StartTime - lastTime)); lastTime = h.GetEndTime(); diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index fd0bff101f..584a9e09c0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -192,7 +192,8 @@ namespace osu.Game.Tests.Gameplay AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); AddAssert("not failed", () => !processor.HasFailed); - AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); + AddStep($"apply {resultApplied.ToString().ToLowerInvariant()} hit result", + () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied })); AddAssert("failed", () => processor.HasFailed); } @@ -232,6 +233,84 @@ namespace osu.Game.Tests.Gameplay assertHealthEqualTo(1); } + [Test] + public void TestNoBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(4.5E-5).Within(0.1E-5)); + } + + [Test] + public void TestSingleBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1500) + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + + [Test] + public void TestOverlappingBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1400), + new BreakPeriod(750, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + + [Test] + public void TestSequentialBreakDrainRate() + { + DrainingHealthProcessor hp = new DrainingHealthProcessor(-1000); + hp.ApplyBeatmap(new Beatmap + { + HitObjects = + { + new JudgeableHitObject { StartTime = 0 }, + new JudgeableHitObject { StartTime = 2000 } + }, + Breaks = + { + new BreakPeriod(500, 1000), + new BreakPeriod(1000, 1500), + } + }); + + Assert.That(hp.DrainRate, Is.EqualTo(9.1E-5).Within(0.1E-5)); + } + private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) { var beatmap = new Beatmap diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index a8808d08e5..3d4fb862fb 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -103,18 +103,20 @@ namespace osu.Game.Rulesets.Scoring if (beatmap.HitObjects.Count > 0) gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); - noDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period( - beatmap.HitObjects - .Select(hitObject => hitObject.GetEndTime()) - .Where(endTime => endTime <= breakPeriod.StartTime) - .DefaultIfEmpty(double.MinValue) - .Last(), - beatmap.HitObjects - .Select(hitObject => hitObject.StartTime) - .Where(startTime => startTime >= breakPeriod.EndTime) - .DefaultIfEmpty(double.MaxValue) - .First() - ))); + noDrainPeriodTracker = new PeriodTracker( + beatmap.Breaks.Select(breakPeriod => + new Period( + beatmap.HitObjects + .Select(hitObject => hitObject.GetEndTime()) + .Where(endTime => endTime <= breakPeriod.StartTime) + .DefaultIfEmpty(double.MinValue) + .Last(), + beatmap.HitObjects + .Select(hitObject => hitObject.StartTime) + .Where(startTime => startTime >= breakPeriod.EndTime) + .DefaultIfEmpty(double.MaxValue) + .First() + ))); targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target); @@ -161,26 +163,24 @@ namespace osu.Game.Rulesets.Scoring { double currentHealth = 1; double lowestHealth = 1; - int currentBreak = -1; + int currentBreak = 0; for (int i = 0; i < healthIncreases.Count; i++) { double currentTime = healthIncreases[i].time; double lastTime = i > 0 ? healthIncreases[i - 1].time : DrainStartTime; - // Subtract any break time from the duration since the last object - if (Beatmap.Breaks.Count > 0) + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= currentTime) { - // Advance the last break occuring before the current time - while (currentBreak + 1 < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak + 1].EndTime < currentTime) - currentBreak++; - - if (currentBreak >= 0) - lastTime = Math.Max(lastTime, Beatmap.Breaks[currentBreak].EndTime); + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = currentTime; + currentBreak++; } // Apply health adjustments - currentHealth -= (healthIncreases[i].time - lastTime) * result; + currentHealth -= (currentTime - lastTime) * result; lowestHealth = Math.Min(lowestHealth, currentHealth); currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); From 69c2c0e4278d4d419b3ee1ea11f4c8e491514247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Nov 2023 16:30:08 +0900 Subject: [PATCH 709/896] Fix touch device mod declared valid for multiplayer Mostly matters for web, so that it doesn't permit creation of playlist items with touch device inside. --- osu.Game/Rulesets/Mods/ModTouchDevice.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModTouchDevice.cs b/osu.Game/Rulesets/Mods/ModTouchDevice.cs index b80b042f11..e91a398700 100644 --- a/osu.Game/Rulesets/Mods/ModTouchDevice.cs +++ b/osu.Game/Rulesets/Mods/ModTouchDevice.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Mods public sealed override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen."; public sealed override double ScoreMultiplier => 1; public sealed override ModType Type => ModType.System; + public sealed override bool ValidForMultiplayer => false; + public sealed override bool ValidForMultiplayerAsFreeMod => false; public sealed override bool AlwaysValidForSubmission => true; public override Type[] IncompatibleMods => new[] { typeof(ICreateReplayData) }; } From 70d2de56695800a2ca14a027d32cf2c36153e8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Nov 2023 16:35:16 +0900 Subject: [PATCH 710/896] Move song select touch detector to solo implementation It should not run in multiplayer. Even if we wanted to allow touch-only playlist items at some point, the current behaviour of multiplayer song selects with respect to touch device mod is currently just broken. --- osu.Game/Screens/Select/PlaySongSelect.cs | 2 ++ osu.Game/Screens/Select/SongSelect.cs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index fe13d6d5a8..86bebdc2ff 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -48,6 +48,8 @@ namespace osu.Game.Screens.Select private void load(OsuColour colours) { BeatmapOptions.AddButton(ButtonSystemStrings.Edit.ToSentence(), @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); + + AddInternal(new SongSelectTouchInputDetector()); } protected void PresentScore(ScoreInfo score) => diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 03083672d5..dfea4e3794 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -279,7 +279,6 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.Both, }, - new SongSelectTouchInputDetector() }); if (ShowFooter) From 90ec6895d100dea67e282c75a57779ffa87d0f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Tue, 14 Nov 2023 16:52:45 +0900 Subject: [PATCH 711/896] Automatic red control point generation & corner threshold --- .../Sliders/SliderPlacementBlueprint.cs | 28 +++++++++-- .../Edit/ISliderDrawingSettingsProvider.cs | 1 + .../Edit/OsuSliderDrawingSettingsProvider.cs | 46 +++++++++++++++++-- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index c722f0fdbc..69e2a40689 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -86,6 +86,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders bSplineBuilder.Tolerance = e.NewValue; updateSliderPathFromBSplineBuilder(); }, true); + + drawingSettingsProvider.CornerThreshold.BindValueChanged(e => + { + if (bSplineBuilder.CornerThreshold != e.NewValue) + bSplineBuilder.CornerThreshold = e.NewValue; + updateSliderPathFromBSplineBuilder(); + }, true); } [Resolved] @@ -100,8 +107,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders case SliderPlacementState.Initial: BeginPlacement(); - double? nearestSliderVelocity = (editorBeatmap.HitObjects - .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier; + double? nearestSliderVelocity = (editorBeatmap + .HitObjects + .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier; HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1; HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); @@ -193,9 +201,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { Scheduler.AddOnce(static self => { - var cps = self.bSplineBuilder.GetControlPoints(); - self.HitObject.Path.ControlPoints.RemoveRange(1, self.HitObject.Path.ControlPoints.Count - 1); - self.HitObject.Path.ControlPoints.AddRange(cps.Skip(1).Select(v => new PathControlPoint(v))); + var cps = self.bSplineBuilder.ControlPoints; + var sliderCps = self.HitObject.Path.ControlPoints; + sliderCps.RemoveRange(1, sliderCps.Count - 1); + + // Add the control points from the BSpline builder while converting control points that repeat + // three or more times to a single PathControlPoint with linear type. + for (int i = 1; i < cps.Count; i++) + { + bool isSharp = i < cps.Count - 2 && cps[i] == cps[i + 1] && cps[i] == cps[i + 2]; + sliderCps.Add(new PathControlPoint(cps[i], isSharp ? PathType.BSpline(3) : null)); + if (isSharp) + i += 2; + } }, this); } diff --git a/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs index 1138588259..31ed98e1dd 100644 --- a/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs @@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Osu.Edit public interface ISliderDrawingSettingsProvider { BindableFloat Tolerance { get; } + BindableFloat CornerThreshold { get; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs index 643732d90a..1fe1326f38 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs @@ -12,20 +12,34 @@ namespace osu.Game.Rulesets.Osu.Edit { public partial class OsuSliderDrawingSettingsProvider : Drawable, ISliderDrawingSettingsProvider, IToolboxAttachment { - public BindableFloat Tolerance { get; } = new BindableFloat(0.1f) + public BindableFloat Tolerance { get; } = new BindableFloat(1.5f) + { + MinValue = 0.05f, + MaxValue = 3f, + Precision = 0.01f + }; + + private readonly BindableInt sliderTolerance = new BindableInt(50) + { + MinValue = 5, + MaxValue = 100 + }; + + public BindableFloat CornerThreshold { get; } = new BindableFloat(0.4f) { MinValue = 0.05f, MaxValue = 1f, Precision = 0.01f }; - private readonly BindableInt sliderTolerance = new BindableInt(10) + private readonly BindableInt sliderCornerThreshold = new BindableInt(40) { MinValue = 5, MaxValue = 100 }; private ExpandableSlider toleranceSlider = null!; + private ExpandableSlider cornerThresholdSlider = null!; protected override void LoadComplete() { @@ -33,16 +47,28 @@ namespace osu.Game.Rulesets.Osu.Edit sliderTolerance.BindValueChanged(v => { - float newValue = v.NewValue / 100f; - if (!Precision.AlmostEquals(newValue, Tolerance.Value)) + float newValue = v.NewValue / 33f; + if (!Precision.AlmostEquals(newValue, Tolerance.Value, 1e-7f)) Tolerance.Value = newValue; }); Tolerance.BindValueChanged(v => { - int newValue = (int)Math.Round(v.NewValue * 100f); + int newValue = (int)Math.Round(v.NewValue * 33f); if (sliderTolerance.Value != newValue) sliderTolerance.Value = newValue; }); + sliderCornerThreshold.BindValueChanged(v => + { + float newValue = v.NewValue / 100f; + if (!Precision.AlmostEquals(newValue, CornerThreshold.Value, 1e-7f)) + CornerThreshold.Value = newValue; + }); + CornerThreshold.BindValueChanged(v => + { + int newValue = (int)Math.Round(v.NewValue * 100f); + if (sliderCornerThreshold.Value != newValue) + sliderCornerThreshold.Value = newValue; + }); } public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer) @@ -54,6 +80,10 @@ namespace osu.Game.Rulesets.Osu.Edit toleranceSlider = new ExpandableSlider { Current = sliderTolerance + }, + cornerThresholdSlider = new ExpandableSlider + { + Current = sliderCornerThreshold } } }); @@ -63,6 +93,12 @@ namespace osu.Game.Rulesets.Osu.Edit toleranceSlider.ContractedLabelText = $"C. P. S.: {e.NewValue:N0}"; toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {e.NewValue:N0}"; }, true); + + sliderCornerThreshold.BindValueChanged(e => + { + cornerThresholdSlider.ContractedLabelText = $"C. T.: {e.NewValue:N0}"; + cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {e.NewValue:N0}"; + }, true); } } } From 83f8f03c7e3b173766e9c715bd7869929082600b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Nov 2023 21:46:57 +0900 Subject: [PATCH 712/896] Fix argon health bar not relative sizing correctly --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index f4ce7d1633..4a5faafd8b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -141,7 +141,13 @@ namespace osu.Game.Screens.Play.HUD Current.BindValueChanged(_ => Scheduler.AddOnce(updateCurrent), true); + // we're about to set `RelativeSizeAxes` depending on the value of `UseRelativeSize`. + // setting `RelativeSizeAxes` internally transforms absolute sizing to relative and back to keep the size the same, + // but that is not what we want in this case, since the width at this point is valid in the *target* sizing mode. + // to counteract this, store the numerical value here, and restore it after setting the correct initial relative sizing axes. + float previousWidth = Width; UseRelativeSize.BindValueChanged(v => RelativeSizeAxes = v.NewValue ? Axes.X : Axes.None, true); + Width = previousWidth; BarHeight.BindValueChanged(_ => updatePath(), true); } From a745642f764cf28b37800936186399f459cdd47d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Nov 2023 22:01:56 +0900 Subject: [PATCH 713/896] Fix customised argon skins no longer loading due to incorrect resource store spec --- .../Screens/Play/HUD/ArgonCounterTextComponent.cs | 14 +++++++------- osu.Game/Skinning/ArgonSkin.cs | 4 +--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 56f60deae1..eabac9a3e6 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -9,11 +9,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Framework.Text; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -133,30 +133,30 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(TextureStore textures) { Spacing = new Vector2(-2f, 0f); Font = new FontUsage(@"argon-counter", 1); - glyphStore = new GlyphStore(skin, getLookup); + glyphStore = new GlyphStore(textures, getLookup); } protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); private class GlyphStore : ITexturedGlyphLookupStore { - private readonly ISkin skin; + private readonly TextureStore textures; private readonly Func getLookup; - public GlyphStore(ISkin skin, Func getLookup) + public GlyphStore(TextureStore textures, Func getLookup) { - this.skin = skin; + this.textures = textures; this.getLookup = getLookup; } public ITexturedCharacterGlyph? Get(string fontName, char character) { string lookup = getLookup(character); - var texture = skin.GetTexture($"{fontName}-{lookup}"); + var texture = textures.Get($"Gameplay/Fonts/{fontName}-{lookup}"); if (texture == null) return null; diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 637467c748..226de4088c 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.Extensions; @@ -44,8 +43,7 @@ namespace osu.Game.Skinning public ArgonSkin(SkinInfo skin, IStorageResourceProvider resources) : base( skin, - resources, - new NamespacedResourceStore(resources.Resources, "Skins/Argon") + resources ) { Resources = resources; From 9a7d7dda2ae564fef11f3e544be339da42abb8d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Nov 2023 23:04:11 +0900 Subject: [PATCH 714/896] 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 81600c1f72..9985afbd8b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + From f5e1734de9c37b1ea9fc8aac79bdf662e5503da0 Mon Sep 17 00:00:00 2001 From: Poyo Date: Tue, 14 Nov 2023 13:51:55 -0800 Subject: [PATCH 715/896] Use soft-cast to access IGameplayClock --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7cfb6aedf0..5abca168ed 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -154,9 +154,6 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } - [Resolved(CanBeNull = true)] - private IGameplayClock gameplayClock { get; set; } - /// /// Whether the initialization logic in has applied. /// @@ -707,7 +704,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Result.RawTime = Time.Current; - Result.GameplayRate = gameplayClock?.GetTrueGameplayRate() ?? 1.0; + Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? 1.0; if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); From 535282ba7d90303058c429c564c9d473afaf8e99 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 14 Nov 2023 14:13:20 -0800 Subject: [PATCH 716/896] Use existing localisation for corner radius in `BoxElement` --- osu.Game/Skinning/Components/BoxElement.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index 235f97ceef..f4f913d80a 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Overlays.Settings; using osuTK; using osuTK.Graphics; @@ -16,7 +18,8 @@ namespace osu.Game.Skinning.Components { public bool UsesFixedAnchor { get; set; } - [SettingSource("Corner rounding", "How round the corners of the box should be.")] + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription), + SettingControlType = typeof(SettingsPercentageSlider))] public BindableFloat CornerRounding { get; } = new BindableFloat(1) { Precision = 0.01f, From 74fb1b5f81475121af75072367759a241aa4db38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Nov 2023 10:40:59 +0900 Subject: [PATCH 717/896] Rename property to match expctations --- osu.Game/Skinning/Components/BoxElement.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index f4f913d80a..8b556418d2 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -20,7 +20,7 @@ namespace osu.Game.Skinning.Components [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription), SettingControlType = typeof(SettingsPercentageSlider))] - public BindableFloat CornerRounding { get; } = new BindableFloat(1) + public new BindableFloat CornerRadius { get; } = new BindableFloat(1) { Precision = 0.01f, MinValue = 0, @@ -47,7 +47,7 @@ namespace osu.Game.Skinning.Components { base.Update(); - CornerRadius = CornerRounding.Value * Math.Min(DrawWidth, DrawHeight) * 0.5f; + base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight) * 0.5f; } } } From 2264e1e2493b69d0455f57639e60e3e2e10f7e96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Nov 2023 10:45:01 +0900 Subject: [PATCH 718/896] Change default value and range of `BoxElement`'s corner radius to match other usages --- osu.Game/Skinning/ArgonSkin.cs | 5 ++++- osu.Game/Skinning/Components/BoxElement.cs | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 226de4088c..4588c62b0f 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -216,7 +216,10 @@ namespace osu.Game.Skinning }, new ArgonScoreCounter(), new ArgonHealthDisplay(), - new BoxElement(), + new BoxElement + { + CornerRadius = { Value = 0.5f } + }, new ArgonAccuracyCounter(), new ArgonComboCounter { diff --git a/osu.Game/Skinning/Components/BoxElement.cs b/osu.Game/Skinning/Components/BoxElement.cs index 8b556418d2..34d389728c 100644 --- a/osu.Game/Skinning/Components/BoxElement.cs +++ b/osu.Game/Skinning/Components/BoxElement.cs @@ -20,11 +20,11 @@ namespace osu.Game.Skinning.Components [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription), SettingControlType = typeof(SettingsPercentageSlider))] - public new BindableFloat CornerRadius { get; } = new BindableFloat(1) + public new BindableFloat CornerRadius { get; } = new BindableFloat(0.25f) { - Precision = 0.01f, MinValue = 0, - MaxValue = 1, + MaxValue = 0.5f, + Precision = 0.01f }; public BoxElement() @@ -47,7 +47,7 @@ namespace osu.Game.Skinning.Components { base.Update(); - base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight) * 0.5f; + base.CornerRadius = CornerRadius.Value * Math.Min(DrawWidth, DrawHeight); } } } From 159cf41f824546013c22ec454fe1bb33a460f2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 15 Nov 2023 12:24:47 +0900 Subject: [PATCH 719/896] Fix default argon health bar width being zero Closes #25460. --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 4a5faafd8b..82203d7891 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -94,6 +94,13 @@ namespace osu.Game.Screens.Play.HUD public ArgonHealthDisplay() { AddLayout(drawSizeLayout); + + // sane default width specification. + // this only matters if the health display isn't part of the default skin + // (in which case width will be set to 300 via `ArgonSkin.GetDrawableComponent()`), + // and if the user hasn't applied their own modifications + // (which are applied via `SerialisedDrawableInfo.ApplySerialisedInfo()`). + Width = 0.98f; } [BackgroundDependencyLoader] From 71714f8f6e6bd7d2caf9214ff267c05e550352e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 15 Nov 2023 13:33:03 +0900 Subject: [PATCH 720/896] Add back test step for testing out argon health bar width --- .../Visual/Gameplay/TestSceneArgonHealthDisplay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs index f51577bc84..6c364e41c7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonHealthDisplay.cs @@ -52,6 +52,12 @@ namespace osu.Game.Tests.Visual.Gameplay if (healthDisplay.IsNotNull()) healthDisplay.BarHeight.Value = val; }); + + AddSliderStep("Width", 0, 1f, 0.98f, val => + { + if (healthDisplay.IsNotNull()) + healthDisplay.Width = val; + }); } [Test] From 87ace7565dfffebbcfb34f48743406a0c68d1065 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 14 Nov 2023 20:44:33 -0800 Subject: [PATCH 721/896] Use existing localisation for argon counter component labels --- osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs | 2 +- osu.Game/Screens/Play/HUD/ArgonComboCounter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index 5f9441a5c4..55fdf24e8f 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Play.HUD new Container { AutoSizeAxes = Axes.Both, - Child = wholePart = new ArgonCounterTextComponent(Anchor.TopRight, "ACCURACY") + Child = wholePart = new ArgonCounterTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper()) { RequiredDisplayDigits = { Value = 3 }, WireframeOpacity = { BindTarget = WireframeOpacity } diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index ac710294ef..52af9b0247 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play.HUD protected override LocalisableString FormatCount(int count) => $@"{count}x"; - protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, "COMBO") + protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) { WireframeOpacity = { BindTarget = WireframeOpacity }, }; From 62352ce5f3b6ce56c310a83dccd11d9eb15f90fa Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 14 Nov 2023 20:45:23 -0800 Subject: [PATCH 722/896] Add ability to toggle labels on argon counter components --- .../SkinnableComponentStrings.cs | 10 +++++++ .../Screens/Play/HUD/ArgonAccuracyCounter.cs | 28 ++++++++++++++++--- .../Screens/Play/HUD/ArgonComboCounter.cs | 7 +++++ .../Play/HUD/ArgonCounterTextComponent.cs | 4 ++- .../Screens/Play/HUD/ArgonScoreCounter.cs | 8 +++++- 5 files changed, 51 insertions(+), 6 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 7c11ea6ac6..639f5c9b16 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -49,6 +49,16 @@ namespace osu.Game.Localisation.SkinComponents /// public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), "How rounded the corners should be."); + /// + /// "Show label" + /// + public static LocalisableString ShowLabel => new TranslatableString(getKey(@"show_label"), @"Show label"); + + /// + /// "Whether the label should be shown." + /// + public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the label should be shown."); + private static string getKey(string key) => $"{prefix}:{key}"; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs index 55fdf24e8f..521ad63426 100644 --- a/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs @@ -2,11 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Resources.Localisation.Web; using osu.Game.Skinning; using osuTK; @@ -25,20 +28,27 @@ namespace osu.Game.Screens.Play.HUD MaxValue = 1, }; + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + public Bindable ShowLabel { get; } = new BindableBool(true); + public bool UsesFixedAnchor { get; set; } protected override IHasText CreateText() => new ArgonAccuracyTextComponent { WireframeOpacity = { BindTarget = WireframeOpacity }, + ShowLabel = { BindTarget = ShowLabel }, }; private partial class ArgonAccuracyTextComponent : CompositeDrawable, IHasText { private readonly ArgonCounterTextComponent wholePart; private readonly ArgonCounterTextComponent fractionPart; + private readonly ArgonCounterTextComponent percentText; public IBindable WireframeOpacity { get; } = new BindableFloat(); + public Bindable ShowLabel { get; } = new BindableBool(); + public LocalisableString Text { get => wholePart.Text; @@ -67,24 +77,34 @@ namespace osu.Game.Screens.Play.HUD Child = wholePart = new ArgonCounterTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper()) { RequiredDisplayDigits = { Value = 3 }, - WireframeOpacity = { BindTarget = WireframeOpacity } + WireframeOpacity = { BindTarget = WireframeOpacity }, + ShowLabel = { BindTarget = ShowLabel }, } }, fractionPart = new ArgonCounterTextComponent(Anchor.TopLeft) { - Margin = new MarginPadding { Top = 12f * 2f + 4f }, // +4 to account for the extra spaces above the digits. WireframeOpacity = { BindTarget = WireframeOpacity }, Scale = new Vector2(0.5f), }, - new ArgonCounterTextComponent(Anchor.TopLeft) + percentText = new ArgonCounterTextComponent(Anchor.TopLeft) { Text = @"%", - Margin = new MarginPadding { Top = 12f }, WireframeOpacity = { BindTarget = WireframeOpacity } }, } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ShowLabel.BindValueChanged(s => + { + fractionPart.Margin = new MarginPadding { Top = s.NewValue ? 12f * 2f + 4f : 4f }; // +4 to account for the extra spaces above the digits. + percentText.Margin = new MarginPadding { Top = s.NewValue ? 12f : 0 }; + }, true); + } } } } diff --git a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs index 52af9b0247..5ea7fd0b82 100644 --- a/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonComboCounter.cs @@ -4,10 +4,13 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Scoring; using osuTK; using osuTK.Graphics; @@ -29,6 +32,9 @@ namespace osu.Game.Screens.Play.HUD MaxValue = 1, }; + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + public Bindable ShowLabel { get; } = new BindableBool(true); + [BackgroundDependencyLoader] private void load(ScoreProcessor scoreProcessor) { @@ -56,6 +62,7 @@ namespace osu.Game.Screens.Play.HUD protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper()) { WireframeOpacity = { BindTarget = WireframeOpacity }, + ShowLabel = { BindTarget = ShowLabel }, }; } } diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index eabac9a3e6..d3fadb452b 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -26,6 +26,7 @@ namespace osu.Game.Screens.Play.HUD public IBindable WireframeOpacity { get; } = new BindableFloat(); public Bindable RequiredDisplayDigits { get; } = new BindableInt(); + public Bindable ShowLabel { get; } = new BindableBool(); public Container NumberContainer { get; private set; } @@ -56,7 +57,7 @@ namespace osu.Game.Screens.Play.HUD { labelText = new OsuSpriteText { - Alpha = label != null ? 1 : 0, + Alpha = 0, Text = label.GetValueOrDefault(), Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold), Margin = new MarginPadding { Left = 2.5f }, @@ -114,6 +115,7 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true); + ShowLabel.BindValueChanged(s => labelText.Alpha = s.NewValue ? 1 : 0, true); } private partial class ArgonCounterSpriteText : OsuSpriteText diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index 0192fa3c02..d661cd67cc 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Localisation.SkinComponents; +using osu.Game.Resources.Localisation.Web; using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD @@ -24,14 +26,18 @@ namespace osu.Game.Screens.Play.HUD MaxValue = 1, }; + [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] + public Bindable ShowLabel { get; } = new BindableBool(); + public bool UsesFixedAnchor { get; set; } protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(); - protected override IHasText CreateText() => new ArgonScoreTextComponent(Anchor.TopRight) + protected override IHasText CreateText() => new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper()) { RequiredDisplayDigits = { BindTarget = RequiredDisplayDigits }, WireframeOpacity = { BindTarget = WireframeOpacity }, + ShowLabel = { BindTarget = ShowLabel }, }; private partial class ArgonScoreTextComponent : ArgonCounterTextComponent From a5e1dd8107a8770ab8698cc39a17df2e81847a8b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Nov 2023 14:07:51 +0900 Subject: [PATCH 723/896] Add test coverage of deserialising a modified Argon skin --- osu.Game.Tests/Skins/SkinDeserialisationTest.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index f68250e0fa..c45eadeff2 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -15,6 +15,7 @@ using osu.Game.IO.Archives; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Skinning; +using osu.Game.Skinning.Components; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Skins @@ -102,6 +103,20 @@ namespace osu.Game.Tests.Skins } } + [Test] + public void TestDeserialiseModifiedArgon() + { + using (var stream = TestResources.OpenResource("Archives/modified-argon-20231106.osk")) + using (var storage = new ZipArchiveReader(stream)) + { + var skin = new TestSkin(new SkinInfo(), null, storage); + + Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2)); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10)); + Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName))); + } + } + [Test] public void TestDeserialiseModifiedClassic() { From aac1854d832ff8ad48333ed0afd968ab50dcf712 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Nov 2023 14:08:05 +0900 Subject: [PATCH 724/896] Add test coverage of layout retrievable after importing modified skins --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index ab3e099c3a..98f7dc5444 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -9,10 +9,12 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Skinning; +using osu.Game.Tests.Resources; using SharpCompress.Archives.Zip; namespace osu.Game.Tests.Skins.IO @@ -21,6 +23,25 @@ namespace osu.Game.Tests.Skins.IO { #region Testing filename metadata inclusion + [TestCase("Archives/modified-classic-20220723.osk")] + [TestCase("Archives/modified-default-20230117.osk")] + [TestCase("Archives/modified-argon-20231106.osk")] + public Task TestImportModifiedSkinHasResources(string archive) => runSkinTest(async osu => + { + using (var stream = TestResources.OpenResource(archive)) + { + var imported = await loadSkinIntoOsu(osu, new ImportTask(stream, "skin.osk")); + + // When the import filename doesn't match, it should be appended (and update the skin.ini). + + var skinManager = osu.Dependencies.Get(); + + skinManager.CurrentSkinInfo.Value = imported; + + Assert.That(skinManager.CurrentSkin.Value.LayoutInfos.Count, Is.EqualTo(2)); + } + }); + [Test] public Task TestSingleImportDifferentFilename() => runSkinTest(async osu => { From ceeaf5b67c527c787b8d34eb3594207ed9ac14d7 Mon Sep 17 00:00:00 2001 From: cs Date: Wed, 15 Nov 2023 07:09:33 +0100 Subject: [PATCH 725/896] CI fixes and small tweaks --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 4 ++-- .../Edit/OsuSliderDrawingSettingsProvider.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 69e2a40689..df7d2c716b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -108,8 +108,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders BeginPlacement(); double? nearestSliderVelocity = (editorBeatmap - .HitObjects - .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier; + .HitObjects + .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocityMultiplier; HitObject.SliderVelocityMultiplier = nearestSliderVelocity ?? 1; HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs index 1fe1326f38..4326b2e943 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit sliderTolerance.BindValueChanged(v => { float newValue = v.NewValue / 33f; - if (!Precision.AlmostEquals(newValue, Tolerance.Value, 1e-7f)) + if (!Precision.AlmostEquals(newValue, Tolerance.Value)) Tolerance.Value = newValue; }); Tolerance.BindValueChanged(v => @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit sliderCornerThreshold.BindValueChanged(v => { float newValue = v.NewValue / 100f; - if (!Precision.AlmostEquals(newValue, CornerThreshold.Value, 1e-7f)) + if (!Precision.AlmostEquals(newValue, CornerThreshold.Value)) CornerThreshold.Value = newValue; }); CornerThreshold.BindValueChanged(v => @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer) { - toolboxContainer.Add(new EditorToolboxGroup("drawing") + toolboxContainer.Add(new EditorToolboxGroup("slider") { Children = new Drawable[] { From 8cd1f08a923b1b38e7b096e59211c0714089d1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 15 Nov 2023 13:33:12 +0900 Subject: [PATCH 726/896] Fix argon health bar folding in on itself --- .../Screens/Play/HUD/ArgonHealthDisplay.cs | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 82203d7891..372d2bab85 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -89,6 +89,11 @@ namespace osu.Game.Screens.Play.HUD public const float MAIN_PATH_RADIUS = 10f; + private const float curve_start_offset = 70; + private const float curve_end_offset = 40; + private const float padding = MAIN_PATH_RADIUS * 2; + private const float curve_smoothness = 10; + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); public ArgonHealthDisplay() @@ -248,11 +253,17 @@ namespace osu.Game.Screens.Play.HUD private void updatePath() { - float barLength = DrawWidth - MAIN_PATH_RADIUS * 2; - float curveStart = barLength - 70; - float curveEnd = barLength - 40; + float usableWidth = DrawWidth - padding; - const float curve_smoothness = 10; + if (usableWidth < 0) enforceMinimumWidth(); + + // the display starts curving at `curve_start_offset` units from the right and ends curving at `curve_end_offset`. + // to ensure that the curve is symmetric when it starts being narrow enough, add a `curve_end_offset` to the left side too. + const float rescale_cutoff = curve_start_offset + curve_end_offset; + + float barLength = Math.Max(DrawWidth - padding, rescale_cutoff); + float curveStart = barLength - curve_start_offset; + float curveEnd = barLength - curve_end_offset; Vector2 diagonalDir = (new Vector2(curveEnd, BarHeight.Value) - new Vector2(curveStart, 0)).Normalized(); @@ -268,6 +279,9 @@ namespace osu.Game.Screens.Play.HUD new PathControlPoint(new Vector2(barLength, BarHeight.Value)), }); + if (DrawWidth - padding < rescale_cutoff) + rescalePathProportionally(); + List vertices = new List(); barPath.GetPathToProgress(vertices, 0.0, 1.0); @@ -276,6 +290,20 @@ namespace osu.Game.Screens.Play.HUD glowBar.Vertices = vertices; updatePathVertices(); + + void enforceMinimumWidth() + { + var relativeAxes = RelativeSizeAxes; + RelativeSizeAxes = Axes.None; + Width = padding; + RelativeSizeAxes = relativeAxes; + } + + void rescalePathProportionally() + { + foreach (var point in barPath.ControlPoints) + point.Position = new Vector2(point.Position.X / barLength * (DrawWidth - padding), point.Position.Y); + } } private void updatePathVertices() From 520642975bda8de23c51e2b350c637a93115b08c Mon Sep 17 00:00:00 2001 From: cs Date: Wed, 15 Nov 2023 07:45:09 +0100 Subject: [PATCH 727/896] Fix control point hover text and context menu --- .../Sliders/Components/PathControlPointPiece.cs | 2 +- .../Sliders/Components/PathControlPointVisualiser.cs | 2 +- osu.Game/Rulesets/Objects/Types/PathType.cs | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs index ac9048d5c7..03792d8520 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -279,6 +279,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } - public LocalisableString TooltipText => ControlPoint.Type.ToString() ?? string.Empty; + public LocalisableString TooltipText => ControlPoint.Type?.Description ?? string.Empty; } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 5ab050ed48..faae966d02 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -403,7 +403,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components int totalCount = Pieces.Count(p => p.IsSelected.Value); int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); - var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ => + var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type!.Value.Description, MenuItemType.Standard, _ => { foreach (var p in Pieces.Where(p => p.IsSelected.Value)) updatePathType(p, type); diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index 4fb48bb8b4..e4249154e5 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; namespace osu.Game.Rulesets.Objects.Types { @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Objects.Types PerfectCurve } - public readonly struct PathType : IEquatable + public readonly struct PathType : IEquatable, IHasDescription { public static readonly PathType CATMULL = new PathType(SplineType.Catmull); public static readonly PathType BEZIER = new PathType(SplineType.BSpline); @@ -31,6 +32,15 @@ namespace osu.Game.Rulesets.Objects.Types ///
public int? Degree { get; init; } + public string Description => Type switch + { + SplineType.Catmull => "Catmull", + SplineType.BSpline => Degree == null ? "Bezier" : "B-Spline", + SplineType.Linear => "Linear", + SplineType.PerfectCurve => "Perfect Curve", + _ => Type.ToString() + }; + public PathType(SplineType splineType) { Type = splineType; From 360864fd7b6dac30bda9a2cf4092a316b280427d Mon Sep 17 00:00:00 2001 From: cs Date: Wed, 15 Nov 2023 07:45:28 +0100 Subject: [PATCH 728/896] Hide catmull curve type when possible --- .../Sliders/Components/PathControlPointVisualiser.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index faae966d02..95c72a0a62 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -371,7 +371,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems.Add(createMenuItemForPathType(PathType.PERFECT_CURVE)); curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER)); curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3))); - curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); + + var hoveredPiece = Pieces.FirstOrDefault(p => p.IsHovered); + + if (hoveredPiece?.ControlPoint.Type == PathType.CATMULL) + curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); var menuItems = new List { From aa9deecafe7ad9150d82bfa7015d7e21cf3888b5 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Fri, 10 Nov 2023 11:37:23 +0100 Subject: [PATCH 729/896] added missing comment, fixed incorrect visibility --- osu.Game/Users/Drawables/ClickableAvatar.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index ef451df95d..1f1960714f 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -33,7 +33,7 @@ namespace osu.Game.Users.Drawables /// A clickable avatar for the specified user, with UI sounds included. ///
/// The user. A null value will get a placeholder avatar. - /// + /// If set to true, the will be shown for the tooltip public ClickableAvatar(APIUser? user = null, bool showCardOnHover = false) { if (user?.Id != APIUser.SYSTEM_USER_ID) @@ -72,7 +72,11 @@ namespace osu.Game.Users.Drawables } protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); - protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint); + protected override void PopOut() + { + this.Delay(150).FadeOut(500, Easing.OutQuint); + Clear(); + } public void Move(Vector2 pos) => Position = pos; From deef8998f73850d0e40247fa5854f042967e4687 Mon Sep 17 00:00:00 2001 From: Joshua Hegedus Date: Fri, 10 Nov 2023 11:45:31 +0100 Subject: [PATCH 730/896] reverted the change --- osu.Game/Users/Drawables/ClickableAvatar.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 1f1960714f..26622a1f30 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -72,11 +72,7 @@ namespace osu.Game.Users.Drawables } protected override void PopIn() => this.FadeIn(150, Easing.OutQuint); - protected override void PopOut() - { - this.Delay(150).FadeOut(500, Easing.OutQuint); - Clear(); - } + protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint); public void Move(Vector2 pos) => Position = pos; From 7b987266d50db8b3c5668a27625f7635a6eeefbc Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Tue, 14 Nov 2023 16:15:22 -0800 Subject: [PATCH 731/896] Change behavior of some clickable avatars in line with web --- osu.Game/Overlays/BeatmapSet/AuthorInfo.cs | 2 +- osu.Game/Overlays/Comments/CommentsContainer.cs | 2 +- osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index 1d01495188..99ad5a5c7d 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapSet AutoSizeAxes = Axes.Both, CornerRadius = 4, Masking = true, - Child = avatar = new UpdateableAvatar(showGuestOnNull: false) + Child = avatar = new UpdateableAvatar(showUserPanelOnHover: true, showGuestOnNull: false) { Size = new Vector2(height), }, diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index af5f4dd280..b4e9a80ff1 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Comments Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 20 }, Children = new Drawable[] { - avatar = new UpdateableAvatar(api.LocalUser.Value) + avatar = new UpdateableAvatar(api.LocalUser.Value, isInteractive: false) { Size = new Vector2(50), CornerExponent = 2, diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index ba1c7ca8b2..ceae17aa5d 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -144,7 +144,7 @@ namespace osu.Game.Overlays.Comments Size = new Vector2(avatar_size), Children = new Drawable[] { - new UpdateableAvatar(Comment.User) + new UpdateableAvatar(Comment.User, showUserPanelOnHover: true) { Size = new Vector2(avatar_size), Masking = true, From b118999120ec13d4632f032429fbc3a26cc43db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 15 Nov 2023 18:27:08 +0900 Subject: [PATCH 732/896] Remove unused using directive --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 98f7dc5444..606a5afac2 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; -using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; From 2987c0e802ec597ab7480c9d3623b423592669bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Nov 2023 19:01:52 +0900 Subject: [PATCH 733/896] Add note about enfocing size methodology --- osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs index 372d2bab85..5e6130d3f8 100644 --- a/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ArgonHealthDisplay.cs @@ -293,9 +293,13 @@ namespace osu.Game.Screens.Play.HUD void enforceMinimumWidth() { - var relativeAxes = RelativeSizeAxes; + // Switch to absolute in order to be able to define a minimum width. + // Then switch back is required. Framework will handle the conversion for us. + Axes relativeAxes = RelativeSizeAxes; RelativeSizeAxes = Axes.None; + Width = padding; + RelativeSizeAxes = relativeAxes; } From a73c870712ff5e2bb6613db29dd8d45d71c1f516 Mon Sep 17 00:00:00 2001 From: Poyo Date: Wed, 15 Nov 2023 17:00:35 -0800 Subject: [PATCH 734/896] Allow GameplayRate to be nullable and assert before use --- osu.Game/Rulesets/Judgements/JudgementResult.cs | 2 +- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- osu.Game/Rulesets/Scoring/HitEvent.cs | 4 ++-- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 5 ++++- osu.Game/Rulesets/UI/Playfield.cs | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 603d470954..db621b4851 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Judgements /// /// The gameplay rate at the time this occurred. /// - public double GameplayRate { get; internal set; } + public double? GameplayRate { get; internal set; } /// /// The combo prior to this occurring. diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5abca168ed..baf13d8911 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -704,7 +704,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Result.RawTime = Time.Current; - Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? 1.0; + Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate(); if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); diff --git a/osu.Game/Rulesets/Scoring/HitEvent.cs b/osu.Game/Rulesets/Scoring/HitEvent.cs index afa654318b..1763190899 100644 --- a/osu.Game/Rulesets/Scoring/HitEvent.cs +++ b/osu.Game/Rulesets/Scoring/HitEvent.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The true gameplay rate at the time of the event. /// - public readonly double GameplayRate; + public readonly double? GameplayRate; /// /// The hit result. @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Scoring /// The that triggered the event. /// The previous . /// A position corresponding to the event. - public HitEvent(double timeOffset, double gameplayRate, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) + public HitEvent(double timeOffset, double? gameplayRate, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position) { TimeOffset = timeOffset; GameplayRate = gameplayRate; diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index a93385ef43..70a11ae760 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace osu.Game.Rulesets.Scoring @@ -18,8 +19,10 @@ namespace osu.Game.Rulesets.Scoring /// public static double? CalculateUnstableRate(this IEnumerable hitEvents) { + Debug.Assert(!hitEvents.Any(ev => ev.GameplayRate == null)); + // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. - double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate).ToArray(); + double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate!.Value).ToArray(); return 10 * standardDeviation(timeOffsets); } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 17baf8838c..eb29d8f30a 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -473,7 +473,7 @@ namespace osu.Game.Rulesets.UI private void onNewResult(DrawableHitObject drawable, JudgementResult result) { - Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null && result.GameplayRate != 0.0); + Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null && result.GameplayRate != null); judgedEntries.Push(drawable.Entry.AsNonNull()); NewResult?.Invoke(drawable, result); From 2b19cf6ce4b908880db4854a4f74d30edd7714ac Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 15 Nov 2023 19:43:25 -0800 Subject: [PATCH 735/896] Fix comment markdown style regression --- .../Visual/Online/TestSceneWikiMarkdownContainer.cs | 3 +-- .../Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs | 3 +-- osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs | 2 +- osu.Game/Overlays/Comments/CommentMarkdownContainer.cs | 4 ++-- osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs | 3 +-- osu.Game/Overlays/Wiki/WikiPanelContainer.cs | 2 +- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs index 0aa0295f7d..d4b6bc2b91 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -10,7 +10,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; @@ -298,7 +297,7 @@ This is a line after the fenced code block! { public LinkInline Link; - public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer { UrlAdded = link => Link = link, }; diff --git a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs index af64913212..b9725de5f4 100644 --- a/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs +++ b/osu.Game/Graphics/Containers/Markdown/Footnotes/OsuMarkdownFootnoteTooltip.cs @@ -5,7 +5,6 @@ using Markdig.Extensions.Footnotes; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Overlays; @@ -62,7 +61,7 @@ namespace osu.Game.Graphics.Containers.Markdown.Footnotes lastFootnote = Text = footnote; } - public override MarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer(); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new FootnoteMarkdownTextFlowContainer(); } private partial class FootnoteMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer diff --git a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs index 5da785603a..b4031752db 100644 --- a/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs +++ b/osu.Game/Graphics/Containers/Markdown/OsuMarkdownContainer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Graphics.Containers.Markdown Font = OsuFont.GetFont(Typeface.Inter, size: 14, weight: FontWeight.Regular), }; - public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer(); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer(); protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new OsuMarkdownHeading(headingBlock); diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index e48a52c787..13446792aa 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Comments protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new CommentMarkdownHeading(headingBlock); - public override MarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer(); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new CommentMarkdownTextFlowContainer(); private partial class CommentMarkdownHeading : OsuMarkdownHeading { @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Comments } } - private partial class CommentMarkdownTextFlowContainer : MarkdownTextFlowContainer + private partial class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer { protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline.Url)); diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs index 9107ad342b..e6bfb75026 100644 --- a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -7,7 +7,6 @@ using Markdig.Extensions.Yaml; using Markdig.Syntax; using Markdig.Syntax.Inlines; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Containers.Markdown; using osu.Game.Graphics.Containers.Markdown; namespace osu.Game.Overlays.Wiki.Markdown @@ -53,7 +52,7 @@ namespace osu.Game.Overlays.Wiki.Markdown base.AddMarkdownComponent(markdownObject, container, level); } - public override MarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer(); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer(); private partial class WikiMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer { diff --git a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs index c5b71cfeb6..cbffe5732e 100644 --- a/osu.Game/Overlays/Wiki/WikiPanelContainer.cs +++ b/osu.Game/Overlays/Wiki/WikiPanelContainer.cs @@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Wiki public override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(Typeface.Torus, weight: FontWeight.Bold)); - public override MarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(f => f.TextAnchor = Anchor.TopCentre); + public override OsuMarkdownTextFlowContainer CreateTextFlow() => base.CreateTextFlow().With(f => f.TextAnchor = Anchor.TopCentre); protected override MarkdownParagraph CreateParagraph(ParagraphBlock paragraphBlock, int level) => base.CreateParagraph(paragraphBlock, level).With(p => p.Margin = new MarginPadding { Bottom = 10 }); From dc2d5749651de89bd1e4d5bba615068bfce1ac30 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Wed, 15 Nov 2023 19:41:13 -0800 Subject: [PATCH 736/896] Fix comment markdown image not showing tooltips --- osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs | 2 +- osu.Game/Overlays/Comments/CommentMarkdownContainer.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs index 97e1cae11c..5e83dd4fb3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDrawableComment.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online new[] { "Plain", "This is plain comment" }, new[] { "Pinned", "This is pinned comment" }, new[] { "Link", "Please visit https://osu.ppy.sh" }, - new[] { "Big Image", "![](Backgrounds/bg1)" }, + new[] { "Big Image", "![](Backgrounds/bg1 \"Big Image\")" }, new[] { "Small Image", "![](Cursor/cursortrail)" }, new[] { diff --git a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs index 13446792aa..51e1b863c7 100644 --- a/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs +++ b/osu.Game/Overlays/Comments/CommentMarkdownContainer.cs @@ -51,12 +51,12 @@ namespace osu.Game.Overlays.Comments private partial class CommentMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer { - protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline.Url)); + protected override void AddImage(LinkInline linkInline) => AddDrawable(new CommentMarkdownImage(linkInline)); - private partial class CommentMarkdownImage : MarkdownImage + private partial class CommentMarkdownImage : OsuMarkdownImage { - public CommentMarkdownImage(string url) - : base(url) + public CommentMarkdownImage(LinkInline linkInline) + : base(linkInline) { } From 39a3313929035bc50e308a5ecee8f568f85dbe76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 14:11:01 +0900 Subject: [PATCH 737/896] Update tooltip description slightly --- .../SkinnableComponentStrings.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs index 639f5c9b16..d5c8d5ccec 100644 --- a/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs +++ b/osu.Game/Localisation/SkinComponents/SkinnableComponentStrings.cs @@ -12,42 +12,42 @@ namespace osu.Game.Localisation.SkinComponents /// /// "Sprite name" /// - public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), "Sprite name"); + public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), @"Sprite name"); /// /// "The filename of the sprite" /// - public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), "The filename of the sprite"); + public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), @"The filename of the sprite"); /// /// "Font" /// - public static LocalisableString Font => new TranslatableString(getKey(@"font"), "Font"); + public static LocalisableString Font => new TranslatableString(getKey(@"font"), @"Font"); /// /// "The font to use." /// - public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), "The font to use."); + public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), @"The font to use."); /// /// "Text" /// - public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), "Text"); + public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), @"Text"); /// /// "The text to be displayed." /// - public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed."); + public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), @"The text to be displayed."); /// /// "Corner radius" /// - public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), "Corner radius"); + public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), @"Corner radius"); /// /// "How rounded the corners should be." /// - public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), "How rounded the corners should be."); + public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), @"How rounded the corners should be."); /// /// "Show label" @@ -55,10 +55,10 @@ namespace osu.Game.Localisation.SkinComponents public static LocalisableString ShowLabel => new TranslatableString(getKey(@"show_label"), @"Show label"); /// - /// "Whether the label should be shown." + /// "Whether the component's label should be shown." /// - public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the label should be shown."); + public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown."); - private static string getKey(string key) => $"{prefix}:{key}"; + private static string getKey(string key) => $@"{prefix}:{key}"; } } From 73eda6c09c259e0f517ffa75a0affb8902ef1fd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 14:18:49 +0900 Subject: [PATCH 738/896] Move non-matching default value to argon skin default speficiation instead --- osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs | 2 +- osu.Game/Skinning/ArgonSkin.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs index d661cd67cc..005f7e36a7 100644 --- a/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/ArgonScoreCounter.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play.HUD }; [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))] - public Bindable ShowLabel { get; } = new BindableBool(); + public Bindable ShowLabel { get; } = new BindableBool(true); public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs index 4588c62b0f..6fcab6a977 100644 --- a/osu.Game/Skinning/ArgonSkin.cs +++ b/osu.Game/Skinning/ArgonSkin.cs @@ -214,7 +214,10 @@ namespace osu.Game.Skinning Size = new Vector2(380, 72), Position = new Vector2(4, 5) }, - new ArgonScoreCounter(), + new ArgonScoreCounter + { + ShowLabel = { Value = false }, + }, new ArgonHealthDisplay(), new BoxElement { From 265ae6fd30697495ab6a3896cd37eac883cfdcaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 15:14:32 +0900 Subject: [PATCH 739/896] Remove unused using refs --- osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 5802f8fc0d..9ed1820045 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Judgements; From 3c513d0b620232e4db4b1849dc869e855c9898b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 15:29:32 +0900 Subject: [PATCH 740/896] Refactor fail reason output to not perform string interpolation unless hooked --- .../Scoring/OsuHealthProcessor.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 9ed1820045..7d6a05026c 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Scoring double currentHp; double currentHpUncapped; - do + while (true) { currentHp = 1; currentHpUncapped = 1; @@ -57,7 +57,6 @@ namespace osu.Game.Rulesets.Osu.Scoring double lastTime = DrainStartTime; int currentBreak = 0; bool fail = false; - string failReason = string.Empty; for (int i = 0; i < Beatmap.HitObjects.Count; i++) { @@ -92,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Scoring { fail = true; testDrop *= 0.96; - failReason = $"hp too low ({currentHp} < {lowestHpEver})"; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); break; } @@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Scoring { fail = true; testDrop *= 0.96; - failReason = $"overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"); break; } @@ -129,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Scoring fail = true; testDrop *= 0.94; hpMultiplierNormal *= 1.01; - failReason = $"end hp too low ({currentHp} < {lowestHpEnd})"; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); } double recovery = (currentHpUncapped - 1) / Beatmap.HitObjects.Count; @@ -139,18 +138,15 @@ namespace osu.Game.Rulesets.Osu.Scoring fail = true; testDrop *= 0.96; hpMultiplierNormal *= 1.01; - failReason = $"recovery too low ({recovery} < {hpRecoveryAvailable})"; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); } - if (fail) + if (!fail) { - OnIterationFail?.Invoke($"FAILED drop {testDrop}: {failReason}"); - continue; + OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); + return testDrop; } - - OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); - return testDrop; - } while (true); + } void reduceHp(double amount) { From dbd4f26436c988a8d1e1afb6e893e82fedb25460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Nov 2023 15:37:53 +0900 Subject: [PATCH 741/896] Use alternative method of scheduling storyboard unload --- osu.Game/Screens/BackgroundScreenStack.cs | 18 ------------------ .../Backgrounds/BackgroundScreenDefault.cs | 6 +++++- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs index 562b212561..99ca383b9f 100644 --- a/osu.Game/Screens/BackgroundScreenStack.cs +++ b/osu.Game/Screens/BackgroundScreenStack.cs @@ -1,12 +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 System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Screens; -using osu.Framework.Threading; -using osu.Game.Screens.Backgrounds; namespace osu.Game.Screens { @@ -36,20 +33,5 @@ namespace osu.Game.Screens base.Push(screen); return true; } - - /// - /// Schedules a delegate to run after 500ms, the time length of a background screen transition. - /// This is used in to dispose of the storyboard once the background screen is completely off-screen. - /// - /// - /// Late storyboard disposals cannot be achieved with any local scheduler from or any component inside it, - /// due to the screen becoming dead at the moment the transition finishes. And, on the frame that it is dead on, it will not receive an , - /// therefore not guaranteeing to dispose the storyboard at any period of time close to the end of the transition. - /// This might require reconsideration framework-side, possibly exposing a "death" event in or all s in general. - /// - /// The delegate - /// - /// - internal ScheduledDelegate ScheduleUntilTransitionEnd(Action action) => Scheduler.AddDelayed(action, BackgroundScreen.TRANSITION_LENGTH); } } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 4583b3e4d6..e46b92795a 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Threading; using osu.Framework.Utils; @@ -36,6 +37,9 @@ namespace osu.Game.Screens.Backgrounds [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private GameHost gameHost { get; set; } + protected virtual bool AllowStoryboardBackground => true; public BackgroundScreenDefault(bool animateOnEnter = true) @@ -81,7 +85,7 @@ namespace osu.Game.Screens.Backgrounds Debug.Assert(backgroundScreenStack != null); if (background is BeatmapBackgroundWithStoryboard storyboardBackground) - storyboardUnloadDelegate = backgroundScreenStack.ScheduleUntilTransitionEnd(storyboardBackground.UnloadStoryboard); + storyboardUnloadDelegate = gameHost.UpdateThread.Scheduler.AddDelayed(storyboardBackground.UnloadStoryboard, TRANSITION_LENGTH); base.OnSuspending(e); } From b88e3cd26f8eb3f7b90e8bc6a96097ae23d3773e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Nov 2023 20:16:23 +0900 Subject: [PATCH 742/896] Change `ResourceStore` provided to `Skin` to be a fallback, not replacement --- .../CatchSkinColourDecodingTest.cs | 4 ++-- .../Formats/LegacyBeatmapEncoderTest.cs | 4 ++-- .../Skins/SkinDeserialisationTest.cs | 4 ++-- .../Skins/TestSceneSkinResources.cs | 4 ++-- osu.Game/Skinning/DefaultLegacySkin.cs | 3 +-- osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 7 +++---- osu.Game/Skinning/Skin.cs | 21 +++++++++++-------- osu.Game/Tests/Visual/SkinnableTestScene.cs | 4 ++-- 9 files changed, 27 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index 72011042bc..74b02bab9b 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestLegacySkin : LegacySkin { - public TestLegacySkin(SkinInfo skin, IResourceStore storage) + public TestLegacySkin(SkinInfo skin, IResourceStore fallbackStore) // Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null). - : base(skin, null, storage) + : base(skin, null, fallbackStore) { } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 5d9049ead7..9ff0fe874f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -174,8 +174,8 @@ namespace osu.Game.Tests.Beatmaps.Formats private class TestLegacySkin : LegacySkin { - public TestLegacySkin(IResourceStore storage, string fileName) - : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName) + public TestLegacySkin(IResourceStore fallbackStore, string fileName) + : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, fallbackStore, fileName) { } } diff --git a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs index c45eadeff2..6423e061c5 100644 --- a/osu.Game.Tests/Skins/SkinDeserialisationTest.cs +++ b/osu.Game.Tests/Skins/SkinDeserialisationTest.cs @@ -149,8 +149,8 @@ namespace osu.Game.Tests.Skins private class TestSkin : Skin { - public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = "skin.ini") - : base(skin, resources, storage, configurationFilename) + public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore = null, string configurationFilename = "skin.ini") + : base(skin, resources, fallbackStore, configurationFilename) { } diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index aaec319b57..e77affd817 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -95,8 +95,8 @@ namespace osu.Game.Tests.Skins { public const string SAMPLE_NAME = "test-sample"; - public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = "skin.ini") - : base(skin, resources, storage, configurationFilename) + public TestSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore = null, string configurationFilename = "skin.ini") + : base(skin, resources, fallbackStore, configurationFilename) { } diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index fd9653e3e5..34ea0af122 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -31,8 +31,7 @@ namespace osu.Game.Skinning : base( skin, resources, - // In the case of the actual default legacy skin (ie. the fallback one, which a user hasn't applied any modifications to) we want to use the game provided resources. - skin.Protected ? new NamespacedResourceStore(resources.Resources, "Skins/Legacy") : null + new NamespacedResourceStore(resources.Resources, "Skins/Legacy") ) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 90eb5fa013..d6ba6ea332 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -73,7 +73,7 @@ namespace osu.Game.Skinning // needs to be removed else it will cause incorrect skin behaviours. This is due to the config lookup having no context of which skin // it should be returning the version for. - Skin.LogLookupDebug(this, lookup, Skin.LookupDebugType.Miss); + LogLookupDebug(this, lookup, LookupDebugType.Miss); return null; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index dc683f1dae..2e91770919 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps.Formats; -using osu.Game.Database; using osu.Game.Extensions; using osu.Game.IO; using osu.Game.Rulesets.Objects.Types; @@ -51,10 +50,10 @@ namespace osu.Game.Skinning /// /// The model for this skin. /// Access to raw game resources. - /// An optional store which will be used for looking up skin resources. If null, one will be created from realm pattern. + /// An optional fallback store which will be used for file lookups that are not serviced by realm user storage. /// The user-facing filename of the configuration file to be parsed. Can accept an .osu or skin.ini file. - protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage, string configurationFilename = @"skin.ini") - : base(skin, resources, storage, configurationFilename) + protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore, string configurationFilename = @"skin.ini") + : base(skin, resources, fallbackStore, configurationFilename) { } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 1e312142d7..9ee69d033d 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -55,7 +55,7 @@ namespace osu.Game.Skinning where TLookup : notnull where TValue : notnull; - private readonly RealmBackedResourceStore? realmBackedStorage; + private readonly ResourceStore store = new ResourceStore(); public string Name { get; } @@ -64,9 +64,9 @@ namespace osu.Game.Skinning /// /// The skin's metadata. Usually a live realm object. /// Access to game-wide resources. - /// An optional store which will *replace* all file lookups that are usually sourced from . + /// An optional fallback store which will be used for file lookups that are not serviced by realm user storage. /// An optional filename to read the skin configuration from. If not provided, the configuration will be retrieved from the storage using "skin.ini". - protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? storage = null, string configurationFilename = @"skin.ini") + protected Skin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore? fallbackStore = null, string configurationFilename = @"skin.ini") { Name = skin.Name; @@ -74,9 +74,9 @@ namespace osu.Game.Skinning { SkinInfo = skin.ToLive(resources.RealmAccess); - storage ??= realmBackedStorage = new RealmBackedResourceStore(SkinInfo, resources.Files, resources.RealmAccess); + store.AddStore(new RealmBackedResourceStore(SkinInfo, resources.Files, resources.RealmAccess)); - var samples = resources.AudioManager?.GetSampleStore(storage); + var samples = resources.AudioManager?.GetSampleStore(store); if (samples != null) { @@ -88,7 +88,7 @@ namespace osu.Game.Skinning } Samples = samples; - Textures = new TextureStore(resources.Renderer, CreateTextureLoaderStore(resources, storage)); + Textures = new TextureStore(resources.Renderer, CreateTextureLoaderStore(resources, store)); } else { @@ -96,7 +96,10 @@ namespace osu.Game.Skinning SkinInfo = skin.ToLiveUnmanaged(); } - var configurationStream = storage?.GetStream(configurationFilename); + if (fallbackStore != null) + store.AddStore(fallbackStore); + + var configurationStream = store.GetStream(configurationFilename); if (configurationStream != null) { @@ -119,7 +122,7 @@ namespace osu.Game.Skinning { string filename = $"{skinnableTarget}.json"; - byte[]? bytes = storage?.Get(filename); + byte[]? bytes = store?.Get(filename); if (bytes == null) continue; @@ -252,7 +255,7 @@ namespace osu.Game.Skinning Textures?.Dispose(); Samples?.Dispose(); - realmBackedStorage?.Dispose(); + store.Dispose(); } #endregion diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index aab1b72990..f371cf721f 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -201,8 +201,8 @@ namespace osu.Game.Tests.Visual { private readonly bool extrapolateAnimations; - public TestLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, bool extrapolateAnimations) - : base(skin, resources, storage) + public TestLegacySkin(SkinInfo skin, IResourceStore fallbackStore, IStorageResourceProvider resources, bool extrapolateAnimations) + : base(skin, resources, fallbackStore) { this.extrapolateAnimations = extrapolateAnimations; } From a1673160f12e08612aa42a9db89f8b9f2e6324f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Nov 2023 16:44:11 +0900 Subject: [PATCH 743/896] Refactor `OsuAutoGenerator` to allow custom SPM specifications --- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 5a3d882ef0..acce8c03e8 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -339,6 +339,10 @@ namespace osu.Game.Rulesets.Osu.Replays AddFrameToReplay(startFrame); + // ~477 as per stable. + const float spin_rpm = 60000f / 20 * (180 / MathF.PI) / 360; + float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000; + switch (h) { // We add intermediate frames for spinning / following a slider here. @@ -354,7 +358,7 @@ namespace osu.Game.Rulesets.Osu.Replays for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame)) { t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection; - angle += (float)t / 20; + angle += (float)t * radsPerMillisecond; Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action)); @@ -363,7 +367,7 @@ namespace osu.Game.Rulesets.Osu.Replays } t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection; - angle += (float)t / 20; + angle += (float)t * radsPerMillisecond; Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); From bd932a5417dd7bc4fba8b237832d556c27ea0d9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Nov 2023 17:07:21 +0900 Subject: [PATCH 744/896] Update localisation analyser Pulls in https://github.com/ppy/osu-localisation-analyser/pull/60. --- .config/dotnet-tools.json | 4 ++-- osu.Game/osu.Game.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 3cecb0d07c..b8dc201559 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,10 +21,10 @@ ] }, "ppy.localisationanalyser.tools": { - "version": "2023.712.0", + "version": "2023.1117.0", "commands": [ "localisation" ] } } -} \ No newline at end of file +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9985afbd8b..10ca49c768 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -31,7 +31,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From c9c8ed7c77332502de4f946affbd8dc107b99dba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Nov 2023 18:41:09 +0900 Subject: [PATCH 745/896] Remove unused values --- osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 7d6a05026c..d1c9227c13 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -197,19 +197,10 @@ namespace osu.Game.Rulesets.Osu.Scoring increase = 0.011; break; - case HitResult.Good: - increase = 0.024; - break; - case HitResult.Great: increase = 0.03; break; - case HitResult.Perfect: - // 1.1 * Great. Unused. - increase = 0.033; - break; - case HitResult.SmallBonus: increase = 0.0085; break; From a556caae437b506b4d26258e0ab69008ccae2ec8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Nov 2023 18:41:54 +0900 Subject: [PATCH 746/896] Move default value out of switch statement --- osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index d1c9227c13..8265ca1c33 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Osu.Scoring private double healthIncreaseFor(HitObject hitObject, HitResult result) { - double increase; + double increase = 0; switch (result) { @@ -208,10 +208,6 @@ namespace osu.Game.Rulesets.Osu.Scoring case HitResult.LargeBonus: increase = 0.01; break; - - default: - increase = 0; - break; } return hpMultiplierNormal * increase; From 2ab84fdaa3f6a73715797d7c9d40d5835153eb68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Nov 2023 18:45:16 +0900 Subject: [PATCH 747/896] Use switch statement for type matching --- .../Scoring/OsuHealthProcessor.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 8265ca1c33..0e0bc1916c 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -99,15 +99,21 @@ namespace osu.Game.Rulesets.Osu.Scoring double hpOverkill = Math.Max(0, hpReduction - currentHp); reduceHp(hpReduction); - if (h is Slider slider) + switch (h) { - foreach (var nested in slider.NestedHitObjects) - increaseHp(nested); - } - else if (h is Spinner spinner) - { - foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) - increaseHp(nested); + case Slider slider: + { + foreach (var nested in slider.NestedHitObjects) + increaseHp(nested); + break; + } + + case Spinner spinner: + { + foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) + increaseHp(nested); + break; + } } // Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners From fd3508254b835c2a9dcc07bdfe72c16cf8cec2f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Nov 2023 18:49:19 +0900 Subject: [PATCH 748/896] Add note about break calculation method --- osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 0e0bc1916c..5a24f29330 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -68,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Scoring // TODO: This doesn't handle overlapping/sequential breaks correctly (/b/614). // Subtract any break time from the duration since the last object + // Note that this method is a bit convoluted, but matches stable code for compatibility. if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) { BreakPeriod e = Beatmap.Breaks[currentBreak]; From 66f7b9fae1e0aaf86609bdbf3315870198e0f8b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Nov 2023 19:09:24 +0900 Subject: [PATCH 749/896] Adjust slider follow circle animation to not abruptly scale on early ticks --- .../Skinning/Argon/ArgonFollowCircle.cs | 9 ++++++--- .../Skinning/Default/DefaultFollowCircle.cs | 9 ++++++--- .../Skinning/Legacy/LegacyFollowCircle.cs | 7 +++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs index fca3e70236..ea21d71d5f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs @@ -88,9 +88,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon protected override void OnSliderTick() { - this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint) - .Then() - .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint); + if (Scale.X >= DrawableSliderBall.FOLLOW_AREA * 0.98f) + { + this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint) + .Then() + .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint); + } } protected override void OnSliderBreak() diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs index 3c41d473f4..4adbfc3928 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultFollowCircle.cs @@ -59,9 +59,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default protected override void OnSliderTick() { - this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint) - .Then() - .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint); + if (Scale.X >= DrawableSliderBall.FOLLOW_AREA * 0.98f) + { + this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint) + .Then() + .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint); + } } protected override void OnSliderBreak() diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs index f8dcb9e8a2..fa2bb9b2ad 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyFollowCircle.cs @@ -44,8 +44,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void OnSliderTick() { - this.ScaleTo(2.2f) - .ScaleTo(2f, 200); + if (Scale.X >= 2f) + { + this.ScaleTo(2.2f) + .ScaleTo(2f, 200); + } } protected override void OnSliderBreak() From 307ec172cbcc49f0edf26aed318d5390666faafa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Nov 2023 23:48:48 +0900 Subject: [PATCH 750/896] Use simplified formula --- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index acce8c03e8..9d63949dcc 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -340,7 +340,7 @@ namespace osu.Game.Rulesets.Osu.Replays AddFrameToReplay(startFrame); // ~477 as per stable. - const float spin_rpm = 60000f / 20 * (180 / MathF.PI) / 360; + const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000; float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000; switch (h) From d9cd546377e7e7b3072fe905b966cebe7eeff746 Mon Sep 17 00:00:00 2001 From: Poyo Date: Sat, 18 Nov 2023 12:09:37 -0800 Subject: [PATCH 751/896] Use rate fallback in DrawableHitObject --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- osu.Game/Rulesets/UI/Playfield.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index baf13d8911..5abca168ed 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -704,7 +704,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Result.RawTime = Time.Current; - Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate(); + Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? 1.0; if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index eb29d8f30a..e9c35555c8 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -473,7 +473,7 @@ namespace osu.Game.Rulesets.UI private void onNewResult(DrawableHitObject drawable, JudgementResult result) { - Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null && result.GameplayRate != null); + Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null); judgedEntries.Push(drawable.Entry.AsNonNull()); NewResult?.Invoke(drawable, result); From bfcca382007be5c91adfecfc08dc0a2fbad4cb36 Mon Sep 17 00:00:00 2001 From: Stedoss <29103029+Stedoss@users.noreply.github.com> Date: Mon, 20 Nov 2023 01:15:26 +0000 Subject: [PATCH 752/896] Handle login API state changes in `UserProfileOverlay` --- osu.Game/Overlays/UserProfileOverlay.cs | 46 ++++++++++++++++++++----- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 0ab842c907..d45a010e4a 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -42,6 +43,11 @@ namespace osu.Game.Overlays private ProfileSectionsContainer? sectionsContainer; private ProfileSectionTabControl? tabs; + private IUser? user; + private IRulesetInfo? ruleset; + + private IBindable apiUser = null!; + [Resolved] private RulesetStore rulesets { get; set; } = null!; @@ -58,17 +64,38 @@ namespace osu.Game.Overlays }); } + [BackgroundDependencyLoader] + private void load() + { + apiUser = API.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(_ => Schedule(() => + { + if (API.IsLoggedIn) + fetchAndSetContent(); + })); + } + protected override ProfileHeader CreateHeader() => new ProfileHeader(); protected override Color4 BackgroundColour => ColourProvider.Background5; - public void ShowUser(IUser user, IRulesetInfo? ruleset = null) + public void ShowUser(IUser userToShow, IRulesetInfo? userRuleset = null) { - if (user.OnlineID == APIUser.SYSTEM_USER_ID) + if (userToShow.OnlineID == APIUser.SYSTEM_USER_ID) return; + user = userToShow; + ruleset = userRuleset; + Show(); + fetchAndSetContent(); + } + + private void fetchAndSetContent() + { + Debug.Assert(user != null); + if (user.OnlineID == Header.User.Value?.User.Id && ruleset?.MatchesOnlineID(Header.User.Value?.Ruleset) == true) return; @@ -143,24 +170,27 @@ namespace osu.Game.Overlays sectionsContainer.ScrollToTop(); + if (!API.IsLoggedIn) + return; + userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); - userReq.Success += u => userLoadComplete(u, ruleset); + userReq.Success += userLoadComplete; API.Queue(userReq); loadingLayer.Show(); } - private void userLoadComplete(APIUser user, IRulesetInfo? ruleset) + private void userLoadComplete(APIUser loadedUser) { Debug.Assert(sections != null && sectionsContainer != null && tabs != null); - var actualRuleset = rulesets.GetRuleset(ruleset?.ShortName ?? user.PlayMode).AsNonNull(); + var actualRuleset = rulesets.GetRuleset(ruleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); - var userProfile = new UserProfileData(user, actualRuleset); + var userProfile = new UserProfileData(loadedUser, actualRuleset); Header.User.Value = userProfile; - if (user.ProfileOrder != null) + if (loadedUser.ProfileOrder != null) { - foreach (string id in user.ProfileOrder) + foreach (string id in loadedUser.ProfileOrder) { var sec = sections.FirstOrDefault(s => s.Identifier == id); From e182acf3e8c012fe36f8317d70ca7d6abe1803e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 11:50:28 +0900 Subject: [PATCH 753/896] Expand comment for clarification --- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 9d63949dcc..1cf6bc91f0 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -339,7 +339,8 @@ namespace osu.Game.Rulesets.Osu.Replays AddFrameToReplay(startFrame); - // ~477 as per stable. + // 0.05 rad/ms, or ~477 RPM, as per stable. + // the redundant conversion from RPM to rad/ms is here for ease of testing custom SPM specs. const float spin_rpm = 0.05f / (2 * MathF.PI) * 60000; float radsPerMillisecond = MathUtils.DegreesToRadians(spin_rpm * 360) / 60000; From 33b592f1c723fb4824f2084337de9dbb940d2b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 12:04:30 +0900 Subject: [PATCH 754/896] Update framework (again) --- 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 1f6a65c450..dd5a8996fb 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 70525a5c59..9a0832b4e7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 577cb9994ce54202bf2c07548389f0e6b701fa8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 12:24:32 +0900 Subject: [PATCH 755/896] Move static instances / construction methods closer together --- osu.Game/Rulesets/Objects/Types/PathType.cs | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index e4249154e5..0e3adb4473 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -16,11 +16,6 @@ namespace osu.Game.Rulesets.Objects.Types public readonly struct PathType : IEquatable, IHasDescription { - public static readonly PathType CATMULL = new PathType(SplineType.Catmull); - public static readonly PathType BEZIER = new PathType(SplineType.BSpline); - public static readonly PathType LINEAR = new PathType(SplineType.Linear); - public static readonly PathType PERFECT_CURVE = new PathType(SplineType.PerfectCurve); - /// /// The type of the spline that should be used to interpret the control points of the path. /// @@ -32,6 +27,25 @@ namespace osu.Game.Rulesets.Objects.Types /// public int? Degree { get; init; } + public PathType(SplineType splineType) + { + Type = splineType; + Degree = null; + } + + public static readonly PathType CATMULL = new PathType(SplineType.Catmull); + public static readonly PathType BEZIER = new PathType(SplineType.BSpline); + public static readonly PathType LINEAR = new PathType(SplineType.Linear); + public static readonly PathType PERFECT_CURVE = new PathType(SplineType.PerfectCurve); + + public static PathType BSpline(int degree) + { + if (degree <= 0) + throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero."); + + return new PathType { Type = SplineType.BSpline, Degree = degree }; + } + public string Description => Type switch { SplineType.Catmull => "Catmull", @@ -41,12 +55,6 @@ namespace osu.Game.Rulesets.Objects.Types _ => Type.ToString() }; - public PathType(SplineType splineType) - { - Type = splineType; - Degree = null; - } - public override int GetHashCode() => HashCode.Combine(Type, Degree); @@ -59,14 +67,6 @@ namespace osu.Game.Rulesets.Objects.Types public static bool operator !=(PathType a, PathType b) => a.Type != b.Type || a.Degree != b.Degree; - public static PathType BSpline(int degree) - { - if (degree <= 0) - throw new ArgumentOutOfRangeException(nameof(degree), "The degree of a B-Spline path must be greater than zero."); - - return new PathType { Type = SplineType.BSpline, Degree = degree }; - } - public bool Equals(PathType other) => Type == other.Type && Degree == other.Degree; } From 25c1a900473de216ba5c07b306b4e49316ec7828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 12:25:44 +0900 Subject: [PATCH 756/896] Change switchexpr to standard switch statement --- osu.Game/Rulesets/Objects/Types/PathType.cs | 29 ++++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index 0e3adb4473..95ddcb8b05 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -46,14 +46,29 @@ namespace osu.Game.Rulesets.Objects.Types return new PathType { Type = SplineType.BSpline, Degree = degree }; } - public string Description => Type switch + public string Description { - SplineType.Catmull => "Catmull", - SplineType.BSpline => Degree == null ? "Bezier" : "B-Spline", - SplineType.Linear => "Linear", - SplineType.PerfectCurve => "Perfect Curve", - _ => Type.ToString() - }; + get + { + switch (Type) + { + case SplineType.Catmull: + return "Catmull"; + + case SplineType.BSpline: + return Degree == null ? "Bezier" : "B-Spline"; + + case SplineType.Linear: + return "Linear"; + + case SplineType.PerfectCurve: + return "Perfect Curve"; + + default: + return Type.ToString(); + } + } + } public override int GetHashCode() => HashCode.Combine(Type, Degree); From 7820c8ce4d558381f80444fcf3fb38035382a852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 12:28:20 +0900 Subject: [PATCH 757/896] Decrease redundancy of equality implementations --- osu.Game/Rulesets/Objects/Types/PathType.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index 95ddcb8b05..37c1d0ab50 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -74,15 +74,12 @@ namespace osu.Game.Rulesets.Objects.Types => HashCode.Combine(Type, Degree); public override bool Equals(object? obj) - => obj is PathType pathType && this == pathType; - - public static bool operator ==(PathType a, PathType b) - => a.Type == b.Type && a.Degree == b.Degree; - - public static bool operator !=(PathType a, PathType b) - => a.Type != b.Type || a.Degree != b.Degree; + => obj is PathType pathType && Equals(pathType); public bool Equals(PathType other) => Type == other.Type && Degree == other.Degree; + + public static bool operator ==(PathType a, PathType b) => a.Equals(b); + public static bool operator !=(PathType a, PathType b) => !a.Equals(b); } } From 518dcc567b14cdf8ade2d449b20cdcbcfcf45e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 12:41:22 +0900 Subject: [PATCH 758/896] Null-check `drawingSettingsProvider` As it's annotated as an optional dependency. --- .../Sliders/SliderPlacementBlueprint.cs | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index df7d2c716b..9b24415604 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -80,19 +80,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.LoadComplete(); inputManager = GetContainingInputManager(); - drawingSettingsProvider.Tolerance.BindValueChanged(e => + if (drawingSettingsProvider != null) { - if (bSplineBuilder.Tolerance != e.NewValue) - bSplineBuilder.Tolerance = e.NewValue; - updateSliderPathFromBSplineBuilder(); - }, true); + drawingSettingsProvider.Tolerance.BindValueChanged(e => + { + if (bSplineBuilder.Tolerance != e.NewValue) + bSplineBuilder.Tolerance = e.NewValue; + updateSliderPathFromBSplineBuilder(); + }, true); - drawingSettingsProvider.CornerThreshold.BindValueChanged(e => - { - if (bSplineBuilder.CornerThreshold != e.NewValue) - bSplineBuilder.CornerThreshold = e.NewValue; - updateSliderPathFromBSplineBuilder(); - }, true); + drawingSettingsProvider.CornerThreshold.BindValueChanged(e => + { + if (bSplineBuilder.CornerThreshold != e.NewValue) + bSplineBuilder.CornerThreshold = e.NewValue; + updateSliderPathFromBSplineBuilder(); + }, true); + } } [Resolved] From f46945a29439716ef10e5e82277cf5e338044094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 12:42:16 +0900 Subject: [PATCH 759/896] Avoid one unnecessary path update from B-spline builder --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 9b24415604..50b4377ccd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (bSplineBuilder.Tolerance != e.NewValue) bSplineBuilder.Tolerance = e.NewValue; updateSliderPathFromBSplineBuilder(); - }, true); + }); drawingSettingsProvider.CornerThreshold.BindValueChanged(e => { From 831884a64b74113f76bc59eed0466f232b71f7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 13:00:12 +0900 Subject: [PATCH 760/896] Remove unused enum member --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 50b4377ccd..50abcf1233 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -344,8 +344,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { Initial, ControlPoints, - Drawing, - DrawingFinalization + Drawing } } } From affef85f2521246412949531e291fdbed4cf1272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 13:02:51 +0900 Subject: [PATCH 761/896] Remove `ISliderDrawingSettingsProvider` Seems like excessive abstraction. --- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- .../Edit/ISliderDrawingSettingsProvider.cs | 13 ------------- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- .../Edit/OsuSliderDrawingSettingsProvider.cs | 2 +- 4 files changed, 3 insertions(+), 16 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 50abcf1233..fb2c1d5149 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private IDistanceSnapProvider distanceSnapProvider { get; set; } [Resolved(CanBeNull = true)] - private ISliderDrawingSettingsProvider drawingSettingsProvider { get; set; } + private OsuSliderDrawingSettingsProvider drawingSettingsProvider { get; set; } private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder(); diff --git a/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs deleted file mode 100644 index 31ed98e1dd..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/ISliderDrawingSettingsProvider.cs +++ /dev/null @@ -1,13 +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.Bindables; - -namespace osu.Game.Rulesets.Osu.Edit -{ - public interface ISliderDrawingSettingsProvider - { - BindableFloat Tolerance { get; } - BindableFloat CornerThreshold { get; } - } -} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index d958b558cf..0dc0d6fd31 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Edit [Cached(typeof(IDistanceSnapProvider))] protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); - [Cached(typeof(ISliderDrawingSettingsProvider))] + [Cached] protected readonly OsuSliderDrawingSettingsProvider SliderDrawingSettingsProvider = new OsuSliderDrawingSettingsProvider(); [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs index 4326b2e943..0126b366d5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Rulesets.Osu.Edit { - public partial class OsuSliderDrawingSettingsProvider : Drawable, ISliderDrawingSettingsProvider, IToolboxAttachment + public partial class OsuSliderDrawingSettingsProvider : Drawable, IToolboxAttachment { public BindableFloat Tolerance { get; } = new BindableFloat(1.5f) { From 5d1bac6d7a55a980e608fbf6bfa8744ceb30b18f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 13:17:43 +0900 Subject: [PATCH 762/896] Remove `IToolboxAttachment` for now The interface doesn't really do anything useful right now because it enforces a common contract, but all usages of the contract go through the concrete implementation, and it inflates the already-huge diff. --- .../Edit/OsuSliderDrawingSettingsProvider.cs | 2 +- osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs | 2 +- osu.Game/Rulesets/Edit/IToolboxAttachment.cs | 10 ---------- 3 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 osu.Game/Rulesets/Edit/IToolboxAttachment.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs index 0126b366d5..e44f0265c8 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Rulesets.Osu.Edit { - public partial class OsuSliderDrawingSettingsProvider : Drawable, IToolboxAttachment + public partial class OsuSliderDrawingSettingsProvider : Drawable { public BindableFloat Tolerance { get; } = new BindableFloat(1.5f) { diff --git a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs index 68411d2b01..ddf539771d 100644 --- a/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/ComposerDistanceSnapProvider.cs @@ -29,7 +29,7 @@ using osu.Game.Screens.Edit.Components.TernaryButtons; namespace osu.Game.Rulesets.Edit { - public abstract partial class ComposerDistanceSnapProvider : Component, IDistanceSnapProvider, IScrollBindingHandler, IToolboxAttachment + public abstract partial class ComposerDistanceSnapProvider : Component, IDistanceSnapProvider, IScrollBindingHandler { private const float adjust_step = 0.1f; diff --git a/osu.Game/Rulesets/Edit/IToolboxAttachment.cs b/osu.Game/Rulesets/Edit/IToolboxAttachment.cs deleted file mode 100644 index 7d7c5980b2..0000000000 --- a/osu.Game/Rulesets/Edit/IToolboxAttachment.cs +++ /dev/null @@ -1,10 +0,0 @@ -// 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.Rulesets.Edit -{ - public interface IToolboxAttachment - { - void AttachToToolbox(ExpandingToolboxContainer toolbox); - } -} From 487326a4c786aa89b597ea191ec83c701386736d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 13:22:36 +0900 Subject: [PATCH 763/896] Remove pattern matching syntax usage in switch Also throw on unknown types. --- osu.Game/Rulesets/Objects/BezierConverter.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 5dc0839d37..4a68161899 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -70,21 +70,21 @@ namespace osu.Game.Rulesets.Objects var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1); var segmentType = controlPoints[start].Type ?? PathType.LINEAR; - switch (segmentType) + switch (segmentType.Type) { - case { Type: SplineType.Catmull }: + case SplineType.Catmull: result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position); break; - case { Type: SplineType.Linear }: + case SplineType.Linear: result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position); break; - case { Type: SplineType.PerfectCurve }: + case SplineType.PerfectCurve: result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position)); break; - default: + case SplineType.BSpline: if (segmentType.Degree != null) throw new NotImplementedException("BSpline conversion of arbitrary degree is not implemented."); @@ -94,6 +94,9 @@ namespace osu.Game.Rulesets.Objects } break; + + default: + throw new ArgumentOutOfRangeException(nameof(segmentType.Type), segmentType.Type, "Unsupported segment type found when converting to legacy Bezier"); } // Start the new segment at the current vertex @@ -160,13 +163,16 @@ namespace osu.Game.Rulesets.Objects break; - default: + case SplineType.BSpline: for (int j = 0; j < segmentVertices.Length - 1; j++) { result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? segmentType : null)); } break; + + default: + throw new ArgumentOutOfRangeException(nameof(segmentType.Type), segmentType.Type, "Unsupported segment type found when converting to legacy Bezier"); } // Start the new segment at the current vertex From 80a3225bb28f315aeceda4dc5e10c2222faa0e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 13:35:07 +0900 Subject: [PATCH 764/896] Use static `BEZIER` instead of allocating new every time --- 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 92a92dca8f..411a9b0d63 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (input.Length > 1 && int.TryParse(input.Substring(1), out int degree) && degree > 0) return PathType.BSpline(degree); - return new PathType(SplineType.BSpline); + return PathType.BEZIER; case 'L': return PathType.LINEAR; From 6d7d826b8b76f775bf59d600d493912a92950540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 15:08:58 +0900 Subject: [PATCH 765/896] Fix incorrect legacy conversion when B-splines are used --- osu.Game/Database/LegacyBeatmapExporter.cs | 3 ++- osu.Game/Rulesets/Objects/BezierConverter.cs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 9ca12a79dd..69120ea885 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -85,7 +85,8 @@ namespace osu.Game.Database if (hasPath.Path.ControlPoints.Count > 1) hasPath.Path.ControlPoints[^1].Type = null; - if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; + if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1 + && hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue; var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs index 4a68161899..638975630e 100644 --- a/osu.Game/Rulesets/Objects/BezierConverter.cs +++ b/osu.Game/Rulesets/Objects/BezierConverter.cs @@ -164,9 +164,13 @@ namespace osu.Game.Rulesets.Objects break; case SplineType.BSpline: - for (int j = 0; j < segmentVertices.Length - 1; j++) + var bSplineResult = segmentType.Degree == null + ? segmentVertices + : PathApproximator.BSplineToBezier(segmentVertices, segmentType.Degree.Value); + + for (int j = 0; j < bSplineResult.Length - 1; j++) { - result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? segmentType : null)); + result.Add(new PathControlPoint(bSplineResult[j], j == 0 ? PathType.BEZIER : null)); } break; From 46d4587c97fc8a6d7b3a2ea0e98366fe149f2ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 15:34:01 +0900 Subject: [PATCH 766/896] Add test for slider drawing --- .../TestSceneSliderPlacementBlueprint.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index ecfc8105f1..d1c94c9c9c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -290,6 +290,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(0, PathType.LINEAR); } + [Test] + public void TestSliderDrawing() + { + addMovementStep(new Vector2(200)); + AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); + + addMovementStep(new Vector2(300, 200)); + addMovementStep(new Vector2(400, 200)); + addMovementStep(new Vector2(400, 300)); + addMovementStep(new Vector2(400)); + addMovementStep(new Vector2(300, 400)); + addMovementStep(new Vector2(200, 400)); + + AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertPlaced(true); + assertLength(600, tolerance: 10); + assertControlPointCount(4); + assertControlPointType(0, PathType.BSpline(3)); + } + [Test] public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior() { @@ -397,7 +418,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected); - private void assertLength(double expected) => AddAssert($"slider length is {expected}", () => Precision.AlmostEquals(expected, getSlider().Distance, 1)); + private void assertLength(double expected, double tolerance = 1) => AddAssert($"slider length is {expected}±{tolerance}", () => Precision.AlmostEquals(expected, getSlider().Distance, tolerance)); private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected); From 5f302662be30eff9eb12318c40651f1f776fb09c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 15:34:23 +0900 Subject: [PATCH 767/896] Remove test terminally broken by introduction of slider drawing --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index d1c94c9c9c..c7a21ba689 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -273,23 +273,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(2, PathType.PERFECT_CURVE); } - [Test] - public void TestBeginPlacementWithoutReleasingMouse() - { - addMovementStep(new Vector2(200)); - AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); - - addMovementStep(new Vector2(400, 200)); - AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left)); - - addClickStep(MouseButton.Right); - - assertPlaced(true); - assertLength(200); - assertControlPointCount(2); - assertControlPointType(0, PathType.LINEAR); - } - [Test] public void TestSliderDrawing() { From 8e39dbbff1898830ab70c5b418ec31b661fede42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 15:41:26 +0900 Subject: [PATCH 768/896] Adjust casing of curve type menu items The "Perfect curve" one in particular... fixes test failures, as some tests were relying on this particular casing. But the new version feels more correct anyway, so it's whatever. --- osu.Game/Rulesets/Objects/Types/PathType.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index 37c1d0ab50..f84d43e3e7 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Objects.Types return "Catmull"; case SplineType.BSpline: - return Degree == null ? "Bezier" : "B-Spline"; + return Degree == null ? "Bezier" : "B-spline"; case SplineType.Linear: return "Linear"; case SplineType.PerfectCurve: - return "Perfect Curve"; + return "Perfect curve"; default: return Type.ToString(); From 43dbd65708de80e4e671e042e76496ffb7526d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 15:53:25 +0900 Subject: [PATCH 769/896] Show Catmull as a control point type option if selection already contains it --- .../TestScenePathControlPointVisualiser.cs | 26 ++++++++++++++++++- .../Components/PathControlPointVisualiser.cs | 4 +-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 811ecf53e9..8234381283 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -148,6 +148,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointPathType(3, null); } + [Test] + public void TestCatmullAvailableIffSelectionContainsCatmull() + { + createVisualiser(true); + + addControlPointStep(new Vector2(200), PathType.CATMULL); + addControlPointStep(new Vector2(300)); + addControlPointStep(new Vector2(500, 300)); + addControlPointStep(new Vector2(700, 200)); + addControlPointStep(new Vector2(500, 100)); + + moveMouseToControlPoint(2); + AddStep("select first and third control point", () => + { + visualiser.Pieces[0].IsSelected.Value = true; + visualiser.Pieces[2].IsSelected.Value = true; + }); + addContextMenuItemStep("Catmull"); + + assertControlPointPathType(0, PathType.CATMULL); + assertControlPointPathType(2, PathType.CATMULL); + assertControlPointPathType(4, null); + } + private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection) { Anchor = Anchor.Centre, @@ -158,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void addControlPointStep(Vector2 position, PathType? type) { - AddStep($"add {type} control point at {position}", () => + AddStep($"add {type?.Type} control point at {position}", () => { slider.Path.ControlPoints.Add(new PathControlPoint(position, type)); }); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 95c72a0a62..751ca7e753 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -372,9 +372,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components curveTypeItems.Add(createMenuItemForPathType(PathType.BEZIER)); curveTypeItems.Add(createMenuItemForPathType(PathType.BSpline(3))); - var hoveredPiece = Pieces.FirstOrDefault(p => p.IsHovered); - - if (hoveredPiece?.ControlPoint.Type == PathType.CATMULL) + if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull)) curveTypeItems.Add(createMenuItemForPathType(PathType.CATMULL)); var menuItems = new List From 4061417ac8cff9242cf5582fdb8b173f7095059e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 15:54:43 +0900 Subject: [PATCH 770/896] Decrease default value for slider tolerance Highly subjective change, but at 50 the drawing just did not feel responsive enough to input. --- osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs index e44f0265c8..7e72eee510 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 0.01f }; - private readonly BindableInt sliderTolerance = new BindableInt(50) + private readonly BindableInt sliderTolerance = new BindableInt(40) { MinValue = 5, MaxValue = 100 From ded9981d07b8bc54fd43a53a76f6335aab5d636f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Nov 2023 17:55:01 +0900 Subject: [PATCH 771/896] 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 dd5a8996fb..8175869405 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 9a0832b4e7..a5425ba4c7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 492fd06c624b2d8d8ff65464798bcde72c4fd51c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Nov 2023 19:21:23 +0900 Subject: [PATCH 772/896] Remove unnecessary null override --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 751ca7e753..3128f46357 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -405,7 +405,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components int totalCount = Pieces.Count(p => p.IsSelected.Value); int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); - var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type!.Value.Description, MenuItemType.Standard, _ => + var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.Value.Description, MenuItemType.Standard, _ => { foreach (var p in Pieces.Where(p => p.IsSelected.Value)) updatePathType(p, type); From c9e8d66e1970fa8498761b4fb5fe868bf879d1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 21:02:12 +0900 Subject: [PATCH 773/896] Improve xmldoc --- osu.Game/OsuGame.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4e37481ba7..3fda18b7cb 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -555,9 +555,9 @@ namespace osu.Game public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version)); /// - /// Seek to a given timestamp in the Editor and select relevant HitObjects if needed + /// Seeks to the provided if the editor is currently open. + /// Can also select objects as indicated by the (depends on ruleset implementation). /// - /// The timestamp and the selected objects public void HandleTimestamp(string timestamp) { if (ScreenStack.CurrentScreen is not Editor editor) From 0e0ab66148ec99a0458b5102541e1a58206a2887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 21:27:24 +0900 Subject: [PATCH 774/896] Simplify parsing code Less methods, less smeared around logic, saner data types. --- osu.Game/Localisation/EditorStrings.cs | 5 --- .../Rulesets/Edit/EditorTimestampParser.cs | 41 ++++++++++++------- osu.Game/Screens/Edit/Editor.cs | 29 +++---------- 3 files changed, 32 insertions(+), 43 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 227dbc5e0c..e762455e8b 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -129,11 +129,6 @@ namespace osu.Game.Localisation ///
public static LocalisableString FailedToProcessTimestamp => new TranslatableString(getKey(@"failed_to_process_timestamp"), @"Failed to process timestamp"); - /// - /// "The timestamp was too long to process" - /// - public static LocalisableString TooLongTimestamp => new TranslatableString(getKey(@"too_long_timestamp"), @"The timestamp was too long to process"); - private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs index e36822cc63..bdfdce432e 100644 --- a/osu.Game/Rulesets/Edit/EditorTimestampParser.cs +++ b/osu.Game/Rulesets/Edit/EditorTimestampParser.cs @@ -2,8 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; namespace osu.Game.Rulesets.Edit @@ -12,26 +11,40 @@ namespace osu.Game.Rulesets.Edit { // 00:00:000 (...) - test // original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78 - public static readonly Regex TIME_REGEX = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\([^)]+\))?)"); + public static readonly Regex TIME_REGEX = new Regex(@"\b(((?\d{2,}):(?[0-5]\d)[:.](?\d{3}))(?\s\([^)]+\))?)", RegexOptions.Compiled); - public static string[] GetRegexGroups(string timestamp) + public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection) { Match match = TIME_REGEX.Match(timestamp); - string[] result = match.Success - ? match.Groups.Values.Where(x => x is not Match && !x.Value.Contains(':')).Select(x => x.Value).ToArray() - : Array.Empty(); + if (!match.Success) + { + parsedTime = null; + parsedSelection = null; + return false; + } - return result; - } + bool result = true; - public static double GetTotalMilliseconds(params string[] timesGroup) - { - int[] times = timesGroup.Select(int.Parse).ToArray(); + result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin); + result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec); + result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec); - Debug.Assert(times.Length == 3); + // somewhat sane limit for timestamp duration (10 hours). + result &= timeMin < 600; - return (times[0] * 60 + times[1]) * 1000 + times[2]; + if (!result) + { + parsedTime = null; + parsedSelection = null; + return false; + } + + parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec); + parsedSelection = match.Groups[@"selection"].Value.Trim(); + if (!string.IsNullOrEmpty(parsedSelection)) + parsedSelection = parsedSelection[1..^1]; + return true; } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 03d3e3a1f8..c38e88cd02 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1141,9 +1141,7 @@ namespace osu.Game.Screens.Edit public void HandleTimestamp(string timestamp) { - string[] groups = EditorTimestampParser.GetRegexGroups(timestamp); - - if (groups.Length != 4 || string.IsNullOrEmpty(groups[0])) + if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection)) { Schedule(() => notifications?.Post(new SimpleNotification { @@ -1153,31 +1151,14 @@ namespace osu.Game.Screens.Edit return; } - string timeMin = groups[0]; - string timeSec = groups[1]; - string timeMss = groups[2]; - string objectsGroup = groups[3].Replace("(", "").Replace(")", "").Trim(); - - // Currently, lazer chat highlights infinite-long editor links like `10000000000:00:000 (1)` - // Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues - if (string.IsNullOrEmpty(timeMin) || timeMin.Length > 5 || double.Parse(timeMin) > 30_000) - { - Schedule(() => notifications?.Post(new SimpleNotification - { - Icon = FontAwesome.Solid.ExclamationTriangle, - Text = EditorStrings.TooLongTimestamp - })); - return; - } - editorBeatmap.SelectedHitObjects.Clear(); if (clock.IsRunning) clock.Stop(); - double position = EditorTimestampParser.GetTotalMilliseconds(timeMin, timeSec, timeMss); + double position = timeSpan.Value.TotalMilliseconds; - if (string.IsNullOrEmpty(objectsGroup)) + if (string.IsNullOrEmpty(selection)) { clock.SeekSmoothlyTo(position); return; @@ -1194,8 +1175,8 @@ namespace osu.Game.Screens.Edit if (Mode.Value != EditorScreenMode.Compose) Mode.Value = EditorScreenMode.Compose; - // Let the Ruleset handle selection - currentScreen.Dependencies.Get().SelectHitObjects(position, objectsGroup); + // Delegate handling the selection to the ruleset. + currentScreen.Dependencies.Get().SelectHitObjects(position, selection); } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From 246aacb216d692b7bc54ae7fccebfd5869561d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 21:29:19 +0900 Subject: [PATCH 775/896] Remove unnecessary guard Setting a bindable's value to something if that value is already there is a no-op (doesn't trigger bindings / callbacks). --- osu.Game/Screens/Edit/Editor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c38e88cd02..888f535cc6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1172,8 +1172,7 @@ namespace osu.Game.Screens.Edit clock.SeekSmoothlyTo(position); - if (Mode.Value != EditorScreenMode.Compose) - Mode.Value = EditorScreenMode.Compose; + Mode.Value = EditorScreenMode.Compose; // Delegate handling the selection to the ruleset. currentScreen.Dependencies.Get().SelectHitObjects(position, selection); From 234ef6f92379b3cbf0c9a07f2334f2a068f9fe95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 21:34:36 +0900 Subject: [PATCH 776/896] Rectify selection keep-alive logic --- .../Components/EditorBlueprintContainer.cs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index a311054ffc..378d378be3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -51,10 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded += AddBlueprintFor; Beatmap.HitObjectRemoved += RemoveBlueprintFor; - - // This makes sure HitObjects will have active Blueprints ready to display - // after clicking on an Editor Timestamp/Link - Beatmap.SelectedHitObjects.CollectionChanged += selectionChanged; + Beatmap.SelectedHitObjects.CollectionChanged += updateSelectionLifetime; if (Composer != null) { @@ -149,13 +146,23 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray()); } - private void selectionChanged(object sender, NotifyCollectionChangedEventArgs e) + /// + /// Ensures that newly-selected hitobjects are kept alive + /// and drops that keep-alive from newly-deselected objects. + /// + private void updateSelectionLifetime(object sender, NotifyCollectionChangedEventArgs e) { - if (e == null || e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null) - return; + if (e.NewItems != null) + { + foreach (HitObject newSelection in e.NewItems) + Composer.Playfield.SetKeepAlive(newSelection, true); + } - foreach (HitObject item in e.NewItems) - Composer.Playfield.SetKeepAlive(item, true); + if (e.OldItems != null) + { + foreach (HitObject oldSelection in e.OldItems) + Composer.Playfield.SetKeepAlive(oldSelection, false); + } } protected override void OnBlueprintSelected(SelectionBlueprint blueprint) @@ -180,7 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Beatmap.HitObjectAdded -= AddBlueprintFor; Beatmap.HitObjectRemoved -= RemoveBlueprintFor; - Beatmap.SelectedHitObjects.CollectionChanged -= selectionChanged; + Beatmap.SelectedHitObjects.CollectionChanged -= updateSelectionLifetime; } usageEventBuffer?.Dispose(); From b6215b2809aac10f2bbf82403b76c6ca37f8d285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 21:37:29 +0900 Subject: [PATCH 777/896] Rename and document `SelectFromTimestamp` --- .../Edit/ManiaHitObjectComposer.cs | 2 +- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 13 +++++++++---- osu.Game/Screens/Edit/Editor.cs | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 92ecea812c..df6c62da51 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit public override string ConvertSelectionToString() => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); - public override void SelectHitObjects(double timestamp, string objectDescription) + public override void SelectFromTimestamp(double timestamp, string objectDescription) { if (!selection_regex.IsMatch(objectDescription)) return; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 2afbd83ce5..585d692978 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Edit public override string ConvertSelectionToString() => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); - public override void SelectHitObjects(double timestamp, string objectDescription) + public override void SelectFromTimestamp(double timestamp, string objectDescription) { if (!selection_regex.IsMatch(objectDescription)) return; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 52a525e84f..50e6393895 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -526,14 +526,19 @@ namespace osu.Game.Rulesets.Edit ///
public abstract bool CursorInPlacementArea { get; } + /// + /// Returns a string representing the current selection. + /// The inverse method to . + /// public virtual string ConvertSelectionToString() => string.Empty; /// - /// Each ruleset has it's own selection method + /// Selects objects based on the supplied and . + /// The inverse method to . /// - /// The given timestamp - /// The selected object information between the brackets - public virtual void SelectHitObjects(double timestamp, string objectDescription) { } + /// The time instant to seek to, in milliseconds. + /// The ruleset-specific description of objects to select at the given timestamp. + public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { } #region IPositionSnapProvider diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 888f535cc6..fb34767315 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1175,7 +1175,7 @@ namespace osu.Game.Screens.Edit Mode.Value = EditorScreenMode.Compose; // Delegate handling the selection to the ruleset. - currentScreen.Dependencies.Get().SelectHitObjects(position, selection); + currentScreen.Dependencies.Get().SelectFromTimestamp(position, selection); } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From c16afeb34700b22a09932ad792ae801c0c5bffb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 21:57:07 +0900 Subject: [PATCH 778/896] Fix tests --- .../Editing/TestSceneOpenEditorTimestamp.cs | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index bc31924e2c..86ceb73f7a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -83,46 +83,42 @@ namespace osu.Game.Tests.Visual.Editing RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; addStepClickLink("00:00:000", waitForSeek: false); - AddAssert("recieved 'must be in edit'", () => - Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 1 - ); + AddAssert("received 'must be in edit'", + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit), + () => Is.EqualTo(1)); AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); - AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); + AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); addStepClickLink("00:00:000 (1)", waitForSeek: false); - AddAssert("recieved 'must be in edit'", () => - Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 2 - ); + AddAssert("received 'must be in edit'", + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit), + () => Is.EqualTo(2)); setUpEditor(rulesetInfo); - AddAssert("is editor Osu", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + AddAssert("ruleset is osu!", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); addStepClickLink("00:000", "invalid link", waitForSeek: false); - AddAssert("recieved 'failed to process'", () => - Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 1 - ); + AddAssert("received 'failed to process'", + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp), + () => Is.EqualTo(1)); addStepClickLink("50000:00:000", "too long link", waitForSeek: false); - AddAssert("recieved 'too long'", () => - Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.TooLongTimestamp) == 1 - ); + AddAssert("received 'failed to process'", + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp), + () => Is.EqualTo(2)); } [Test] public void TestHandleCurrentScreenChanges() { - const long long_link_value = 1_000 * 60 * 1_000; RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; setUpEditor(rulesetInfo); - AddAssert("is editor Osu", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); + AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); - addStepClickLink("1000:00:000", "long link"); - AddAssert("moved to end of track", () => - editorClock.CurrentTime == long_link_value - || (editorClock.TrackLength < long_link_value && editorClock.CurrentTime == editorClock.TrackLength) - ); + addStepClickLink("100:00:000", "long link"); + AddUntilStep("moved to end of track", () => editorClock.CurrentTime, () => Is.EqualTo(editorClock.TrackLength)); addStepScreenModeTo(EditorScreenMode.SongSetup); addStepClickLink("00:00:000"); From 364a3f75e15001b785f185c41ba69e28edecf926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 22:03:25 +0900 Subject: [PATCH 779/896] Compile regexes --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 6 +++--- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index df6c62da51..e876ff1c81 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -49,12 +49,12 @@ namespace osu.Game.Rulesets.Mania.Edit new HoldNoteCompositionTool() }; - // 123|0,456|1,789|2 ... - private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$"); - public override string ConvertSelectionToString() => string.Join(',', EditorBeatmap.SelectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}")); + // 123|0,456|1,789|2 ... + private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled); + public override void SelectFromTimestamp(double timestamp, string objectDescription) { if (!selection_regex.IsMatch(objectDescription)) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 585d692978..c3fd36b933 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -104,12 +104,12 @@ namespace osu.Game.Rulesets.Osu.Edit protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(this); - // 1,2,3,4 ... - private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$"); - public override string ConvertSelectionToString() => string.Join(',', selectedHitObjects.Cast().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString())); + // 1,2,3,4 ... + private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled); + public override void SelectFromTimestamp(double timestamp, string objectDescription) { if (!selection_regex.IsMatch(objectDescription)) From a8fc73695f30b3d22c260c8ea030f3034d1c21d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 22:04:08 +0900 Subject: [PATCH 780/896] Rename variable --- osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index e876ff1c81..967cdb0e54 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -61,11 +61,11 @@ namespace osu.Game.Rulesets.Mania.Edit return; List remainingHitObjects = EditorBeatmap.HitObjects.Cast().Where(h => h.StartTime >= timestamp).ToList(); - string[] splitDescription = objectDescription.Split(',').ToArray(); + string[] objectDescriptions = objectDescription.Split(',').ToArray(); - for (int i = 0; i < splitDescription.Length; i++) + for (int i = 0; i < objectDescriptions.Length; i++) { - string[] split = splitDescription[i].Split('|').ToArray(); + string[] split = objectDescriptions[i].Split('|').ToArray(); if (split.Length != 2) continue; @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Mania.Edit EditorBeatmap.SelectedHitObjects.Add(current); - if (i < splitDescription.Length - 1) + if (i < objectDescriptions.Length - 1) remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList(); } } From 745a04a243b90a1b83da308ea3e8e6467c9faacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 20 Nov 2023 22:12:15 +0900 Subject: [PATCH 781/896] More test cleanup --- .../TestSceneOpenEditorTimestampInOsu.cs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs index 93573b5ad8..753400e10e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.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 NUnit.Framework; @@ -25,16 +26,17 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addStepClickLink("00:00:000", "reset", false); } - private bool checkSnapAndSelectCombo(double startTime, params int[] comboNumbers) - { - bool checkCombos = comboNumbers.Any() - ? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers) - : !EditorBeatmap.SelectedHitObjects.Any(); + private void checkSelection(Func startTime, params int[] comboNumbers) + => AddUntilStep($"seeked & selected {(comboNumbers.Any() ? string.Join(",", comboNumbers) : "nothing")}", () => + { + bool checkCombos = comboNumbers.Any() + ? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers) + : !EditorBeatmap.SelectedHitObjects.Any(); - return EditorClock.CurrentTime == startTime - && EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length - && checkCombos; - } + return EditorClock.CurrentTime == startTime() + && EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length + && checkCombos; + }); private bool hasCombosInOrder(IEnumerable selected, params int[] comboNumbers) { @@ -51,19 +53,19 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public void TestNormalSelection() { addStepClickLink("00:02:170 (1,2,3)"); - AddAssert("snap and select 1-2-3", () => checkSnapAndSelectCombo(2_170, 1, 2, 3)); + checkSelection(() => 2_170, 1, 2, 3); addReset(); addStepClickLink("00:04:748 (2,3,4,1,2)"); - AddAssert("snap and select 2-3-4-1-2", () => checkSnapAndSelectCombo(4_748, 2, 3, 4, 1, 2)); + checkSelection(() => 4_748, 2, 3, 4, 1, 2); addReset(); addStepClickLink("00:02:170 (1,1,1)"); - AddAssert("snap and select 1-1-1", () => checkSnapAndSelectCombo(2_170, 1, 1, 1)); + checkSelection(() => 2_170, 1, 1, 1); addReset(); addStepClickLink("00:02:873 (2,2,2,2)"); - AddAssert("snap and select 2-2-2-2", () => checkSnapAndSelectCombo(2_873, 2, 2, 2, 2)); + checkSelection(() => 2_873, 2, 2, 2, 2); } [Test] @@ -71,24 +73,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { HitObject firstObject = null!; + AddStep("retrieve first object", () => firstObject = EditorBeatmap.HitObjects.First()); + addStepClickLink("00:00:000 (0)", "invalid combo"); - AddAssert("snap to next, select none", () => - { - firstObject = EditorBeatmap.HitObjects.First(); - return checkSnapAndSelectCombo(firstObject.StartTime); - }); + checkSelection(() => firstObject.StartTime); addReset(); addStepClickLink("00:00:000 (1)", "wrong offset"); - AddAssert("snap and select 1", () => checkSnapAndSelectCombo(firstObject.StartTime, 1)); + checkSelection(() => firstObject.StartTime, 1); addReset(); addStepClickLink("00:00:956 (2,3,4)", "wrong offset"); - AddAssert("snap to next, select 2-3-4", () => checkSnapAndSelectCombo(firstObject.StartTime, 2, 3, 4)); + checkSelection(() => firstObject.StartTime, 2, 3, 4); addReset(); addStepClickLink("00:00:956 (956|1,956|2)", "mania link"); - AddAssert("snap to next, select none", () => checkSnapAndSelectCombo(firstObject.StartTime)); + checkSelection(() => firstObject.StartTime); } } } From 750bbc8a19cfdb0f1c3133e9bd86e02bacb51f0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 00:17:25 +0900 Subject: [PATCH 782/896] Simplify null checks --- .../Sliders/Components/PathControlPointVisualiser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 3128f46357..ef8a033014 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint); - if (type.HasValue && type.Value.Type == SplineType.PerfectCurve) + if (type?.Type == SplineType.PerfectCurve) { // Can't always create a circular arc out of 4 or more points, // so we split the segment into one 3-point circular arc segment @@ -405,7 +405,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components int totalCount = Pieces.Count(p => p.IsSelected.Value); int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type); - var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.Value.Description, MenuItemType.Standard, _ => + var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => { foreach (var p in Pieces.Where(p => p.IsSelected.Value)) updatePathType(p, type); From 6f5c468a83cd0341616d01d10ef5345568a7d943 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 00:21:44 +0900 Subject: [PATCH 783/896] Rename settings class --- .../Blueprints/Sliders/SliderPlacementBlueprint.cs | 8 ++++---- ...ovider.cs => FreehandSliderSettingsProvider.cs} | 14 +++++++------- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) rename osu.Game.Rulesets.Osu/Edit/{OsuSliderDrawingSettingsProvider.cs => FreehandSliderSettingsProvider.cs} (98%) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index fb2c1d5149..df8417d8ff 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private IDistanceSnapProvider distanceSnapProvider { get; set; } [Resolved(CanBeNull = true)] - private OsuSliderDrawingSettingsProvider drawingSettingsProvider { get; set; } + private FreehandSliderSettingsProvider freehandSettingsProvider { get; set; } private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder(); @@ -80,16 +80,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.LoadComplete(); inputManager = GetContainingInputManager(); - if (drawingSettingsProvider != null) + if (freehandSettingsProvider != null) { - drawingSettingsProvider.Tolerance.BindValueChanged(e => + freehandSettingsProvider.Tolerance.BindValueChanged(e => { if (bSplineBuilder.Tolerance != e.NewValue) bSplineBuilder.Tolerance = e.NewValue; updateSliderPathFromBSplineBuilder(); }); - drawingSettingsProvider.CornerThreshold.BindValueChanged(e => + freehandSettingsProvider.CornerThreshold.BindValueChanged(e => { if (bSplineBuilder.CornerThreshold != e.NewValue) bSplineBuilder.CornerThreshold = e.NewValue; diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs similarity index 98% rename from osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs rename to osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs index 7e72eee510..9ad2b5d0f5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSliderDrawingSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Rulesets.Osu.Edit { - public partial class OsuSliderDrawingSettingsProvider : Drawable + public partial class FreehandSliderSettingsProvider : Drawable { public BindableFloat Tolerance { get; } = new BindableFloat(1.5f) { @@ -19,12 +19,6 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 0.01f }; - private readonly BindableInt sliderTolerance = new BindableInt(40) - { - MinValue = 5, - MaxValue = 100 - }; - public BindableFloat CornerThreshold { get; } = new BindableFloat(0.4f) { MinValue = 0.05f, @@ -32,6 +26,12 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 0.01f }; + private readonly BindableInt sliderTolerance = new BindableInt(40) + { + MinValue = 5, + MaxValue = 100 + }; + private readonly BindableInt sliderCornerThreshold = new BindableInt(40) { MinValue = 5, diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0dc0d6fd31..7bf1a12149 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); [Cached] - protected readonly OsuSliderDrawingSettingsProvider SliderDrawingSettingsProvider = new OsuSliderDrawingSettingsProvider(); + protected readonly FreehandSliderSettingsProvider FreehandSliderSettingsProvider = new FreehandSliderSettingsProvider(); [BackgroundDependencyLoader] private void load() @@ -102,8 +102,8 @@ namespace osu.Game.Rulesets.Osu.Edit RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }); - AddInternal(SliderDrawingSettingsProvider); - SliderDrawingSettingsProvider.AttachToToolbox(RightToolbox); + AddInternal(FreehandSliderSettingsProvider); + FreehandSliderSettingsProvider.AttachToToolbox(RightToolbox); } protected override ComposeBlueprintContainer CreateBlueprintContainer() From 638c8f1adc2e135aff574e135c515adfbf384423 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 00:25:23 +0900 Subject: [PATCH 784/896] Get rid of weird cruft and non-standard flow --- .../Edit/FreehandSliderSettingsProvider.cs | 66 ++++++++++--------- .../Edit/OsuHitObjectComposer.cs | 5 +- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs index 9ad2b5d0f5..f4ee130938 100644 --- a/osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs @@ -2,6 +2,7 @@ // 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.Utils; @@ -10,8 +11,13 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Rulesets.Osu.Edit { - public partial class FreehandSliderSettingsProvider : Drawable + public partial class FreehandSliderSettingsProvider : EditorToolboxGroup { + public FreehandSliderSettingsProvider() + : base("slider") + { + } + public BindableFloat Tolerance { get; } = new BindableFloat(1.5f) { MinValue = 0.05f, @@ -41,6 +47,34 @@ namespace osu.Game.Rulesets.Osu.Edit private ExpandableSlider toleranceSlider = null!; private ExpandableSlider cornerThresholdSlider = null!; + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + toleranceSlider = new ExpandableSlider + { + Current = sliderTolerance + }, + cornerThresholdSlider = new ExpandableSlider + { + Current = sliderCornerThreshold + } + }; + + sliderTolerance.BindValueChanged(e => + { + toleranceSlider.ContractedLabelText = $"C. P. S.: {e.NewValue:N0}"; + toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {e.NewValue:N0}"; + }, true); + + sliderCornerThreshold.BindValueChanged(e => + { + cornerThresholdSlider.ContractedLabelText = $"C. T.: {e.NewValue:N0}"; + cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {e.NewValue:N0}"; + }, true); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -70,35 +104,5 @@ namespace osu.Game.Rulesets.Osu.Edit sliderCornerThreshold.Value = newValue; }); } - - public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer) - { - toolboxContainer.Add(new EditorToolboxGroup("slider") - { - Children = new Drawable[] - { - toleranceSlider = new ExpandableSlider - { - Current = sliderTolerance - }, - cornerThresholdSlider = new ExpandableSlider - { - Current = sliderCornerThreshold - } - } - }); - - sliderTolerance.BindValueChanged(e => - { - toleranceSlider.ContractedLabelText = $"C. P. S.: {e.NewValue:N0}"; - toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {e.NewValue:N0}"; - }, true); - - sliderCornerThreshold.BindValueChanged(e => - { - cornerThresholdSlider.ContractedLabelText = $"C. T.: {e.NewValue:N0}"; - cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {e.NewValue:N0}"; - }, true); - } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 7bf1a12149..06584ef17a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); [Cached] - protected readonly FreehandSliderSettingsProvider FreehandSliderSettingsProvider = new FreehandSliderSettingsProvider(); + protected readonly FreehandSliderSettingsProvider FreehandlSliderToolboxGroup = new FreehandSliderSettingsProvider(); [BackgroundDependencyLoader] private void load() @@ -102,8 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }); - AddInternal(FreehandSliderSettingsProvider); - FreehandSliderSettingsProvider.AttachToToolbox(RightToolbox); + RightToolbox.Add(FreehandlSliderToolboxGroup); } protected override ComposeBlueprintContainer CreateBlueprintContainer() From e9f371a5811b72763a50fc1e5fffba5ef95e081f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 09:59:49 +0900 Subject: [PATCH 785/896] Refactor slider settings class --- .../Sliders/SliderPlacementBlueprint.cs | 8 +- ...vider.cs => FreehandSliderToolboxGroup.cs} | 74 +++++++++---------- .../Edit/OsuHitObjectComposer.cs | 2 +- 3 files changed, 38 insertions(+), 46 deletions(-) rename osu.Game.Rulesets.Osu/Edit/{FreehandSliderSettingsProvider.cs => FreehandSliderToolboxGroup.cs} (53%) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index df8417d8ff..f8b7642643 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private IDistanceSnapProvider distanceSnapProvider { get; set; } [Resolved(CanBeNull = true)] - private FreehandSliderSettingsProvider freehandSettingsProvider { get; set; } + private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; } private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder(); @@ -80,16 +80,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.LoadComplete(); inputManager = GetContainingInputManager(); - if (freehandSettingsProvider != null) + if (freehandToolboxGroup != null) { - freehandSettingsProvider.Tolerance.BindValueChanged(e => + freehandToolboxGroup.Tolerance.BindValueChanged(e => { if (bSplineBuilder.Tolerance != e.NewValue) bSplineBuilder.Tolerance = e.NewValue; updateSliderPathFromBSplineBuilder(); }); - freehandSettingsProvider.CornerThreshold.BindValueChanged(e => + freehandToolboxGroup.CornerThreshold.BindValueChanged(e => { if (bSplineBuilder.CornerThreshold != e.NewValue) bSplineBuilder.CornerThreshold = e.NewValue; diff --git a/osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs similarity index 53% rename from osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs rename to osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs index f4ee130938..1974415d30 100644 --- a/osu.Game.Rulesets.Osu/Edit/FreehandSliderSettingsProvider.cs +++ b/osu.Game.Rulesets.Osu/Edit/FreehandSliderToolboxGroup.cs @@ -5,15 +5,14 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; namespace osu.Game.Rulesets.Osu.Edit { - public partial class FreehandSliderSettingsProvider : EditorToolboxGroup + public partial class FreehandSliderToolboxGroup : EditorToolboxGroup { - public FreehandSliderSettingsProvider() + public FreehandSliderToolboxGroup() : base("slider") { } @@ -32,13 +31,14 @@ namespace osu.Game.Rulesets.Osu.Edit Precision = 0.01f }; - private readonly BindableInt sliderTolerance = new BindableInt(40) + // We map internal ranges to a more standard range of values for display to the user. + private readonly BindableInt displayTolerance = new BindableInt(40) { MinValue = 5, MaxValue = 100 }; - private readonly BindableInt sliderCornerThreshold = new BindableInt(40) + private readonly BindableInt displayCornerThreshold = new BindableInt(40) { MinValue = 5, MaxValue = 100 @@ -54,55 +54,47 @@ namespace osu.Game.Rulesets.Osu.Edit { toleranceSlider = new ExpandableSlider { - Current = sliderTolerance + Current = displayTolerance }, cornerThresholdSlider = new ExpandableSlider { - Current = sliderCornerThreshold + Current = displayCornerThreshold } }; - - sliderTolerance.BindValueChanged(e => - { - toleranceSlider.ContractedLabelText = $"C. P. S.: {e.NewValue:N0}"; - toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {e.NewValue:N0}"; - }, true); - - sliderCornerThreshold.BindValueChanged(e => - { - cornerThresholdSlider.ContractedLabelText = $"C. T.: {e.NewValue:N0}"; - cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {e.NewValue:N0}"; - }, true); } protected override void LoadComplete() { base.LoadComplete(); - sliderTolerance.BindValueChanged(v => + displayTolerance.BindValueChanged(tolerance => { - float newValue = v.NewValue / 33f; - if (!Precision.AlmostEquals(newValue, Tolerance.Value)) - Tolerance.Value = newValue; - }); - Tolerance.BindValueChanged(v => + toleranceSlider.ContractedLabelText = $"C. P. S.: {tolerance.NewValue:N0}"; + toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {tolerance.NewValue:N0}"; + + Tolerance.Value = displayToInternalTolerance(tolerance.NewValue); + }, true); + + displayCornerThreshold.BindValueChanged(threshold => { - int newValue = (int)Math.Round(v.NewValue * 33f); - if (sliderTolerance.Value != newValue) - sliderTolerance.Value = newValue; - }); - sliderCornerThreshold.BindValueChanged(v => - { - float newValue = v.NewValue / 100f; - if (!Precision.AlmostEquals(newValue, CornerThreshold.Value)) - CornerThreshold.Value = newValue; - }); - CornerThreshold.BindValueChanged(v => - { - int newValue = (int)Math.Round(v.NewValue * 100f); - if (sliderCornerThreshold.Value != newValue) - sliderCornerThreshold.Value = newValue; - }); + cornerThresholdSlider.ContractedLabelText = $"C. T.: {threshold.NewValue:N0}"; + cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {threshold.NewValue:N0}"; + + CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue); + }, true); + + Tolerance.BindValueChanged(tolerance => + displayTolerance.Value = internalToDisplayTolerance(tolerance.NewValue) + ); + CornerThreshold.BindValueChanged(threshold => + displayCornerThreshold.Value = internalToDisplayCornerThreshold(threshold.NewValue) + ); + + float displayToInternalTolerance(float v) => v / 33f; + int internalToDisplayTolerance(float v) => (int)Math.Round(v * 33f); + + float displayToInternalCornerThreshold(float v) => v / 100f; + int internalToDisplayCornerThreshold(float v) => (int)Math.Round(v * 100f); } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 06584ef17a..061f72d72f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider(); [Cached] - protected readonly FreehandSliderSettingsProvider FreehandlSliderToolboxGroup = new FreehandSliderSettingsProvider(); + protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup(); [BackgroundDependencyLoader] private void load() From 5514a53df171d035d7d3afdee56b38903f29dc07 Mon Sep 17 00:00:00 2001 From: Stedoss <29103029+Stedoss@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:04:46 +0000 Subject: [PATCH 786/896] Pass ruleset to callback to prevent ruleset desync --- osu.Game/Overlays/UserProfileOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index d45a010e4a..fd494f63e1 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -174,16 +174,16 @@ namespace osu.Game.Overlays return; userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); - userReq.Success += userLoadComplete; + userReq.Success += u => userLoadComplete(u, ruleset); API.Queue(userReq); loadingLayer.Show(); } - private void userLoadComplete(APIUser loadedUser) + private void userLoadComplete(APIUser loadedUser, IRulesetInfo? userRuleset) { Debug.Assert(sections != null && sectionsContainer != null && tabs != null); - var actualRuleset = rulesets.GetRuleset(ruleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); + var actualRuleset = rulesets.GetRuleset(userRuleset?.ShortName ?? loadedUser.PlayMode).AsNonNull(); var userProfile = new UserProfileData(loadedUser, actualRuleset); Header.User.Value = userProfile; From ec7b82f5e874e7728a10ba5fe5c5872218dac056 Mon Sep 17 00:00:00 2001 From: Stedoss <29103029+Stedoss@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:55:08 +0000 Subject: [PATCH 787/896] Change early return to check for online `State` instead of `IsLoggedIn` --- osu.Game/Overlays/UserProfileOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index fd494f63e1..193651fa72 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Profile; @@ -170,7 +171,7 @@ namespace osu.Game.Overlays sectionsContainer.ScrollToTop(); - if (!API.IsLoggedIn) + if (API.State.Value != APIState.Online) return; userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); From 826c82de474f1cf322500dcc10e2d2d38719e297 Mon Sep 17 00:00:00 2001 From: Stedoss <29103029+Stedoss@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:56:37 +0000 Subject: [PATCH 788/896] Add unit test for handling login state in `UserProfileOverlay` --- .../Online/TestSceneUserProfileOverlay.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index a321a194a9..1375689075 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -78,6 +78,31 @@ namespace osu.Game.Tests.Visual.Online AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER)); } + [Test] + public void TestLogin() + { + GetUserRequest pendingRequest = null!; + + AddStep("set up request handling", () => + { + dummyAPI.HandleRequest = req => + { + if (dummyAPI.State.Value == APIState.Online && req is GetUserRequest getUserRequest) + { + pendingRequest = getUserRequest; + return true; + } + + return false; + }; + }); + AddStep("logout", () => dummyAPI.Logout()); + AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 })); + AddStep("login", () => dummyAPI.Login("username", "password")); + AddWaitStep("wait some", 3); + AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER)); + } + public static readonly APIUser TEST_USER = new APIUser { Username = @"Somebody", From 3680024e3102d5dcca617371105d4c3bf3787945 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 11:15:00 +0900 Subject: [PATCH 789/896] Fix tolerance not being transferred to blueprint in all cases --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index f8b7642643..5f072eb171 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (bSplineBuilder.Tolerance != e.NewValue) bSplineBuilder.Tolerance = e.NewValue; updateSliderPathFromBSplineBuilder(); - }); + }, true); freehandToolboxGroup.CornerThreshold.BindValueChanged(e => { From 405ab499e9dfd7c75477513aa38b344ffb901781 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:24:10 +0900 Subject: [PATCH 790/896] Allow context menus to have visible spacers --- .../Visual/Editing/TestSceneEditorMenuBar.cs | 22 +++++++------- osu.Game/Graphics/UserInterface/OsuMenu.cs | 30 ++++++++++++++++++- .../UserInterface/OsuMenuItemSpacer.cs | 13 ++++++++ osu.Game/Overlays/SkinEditor/SkinEditor.cs | 10 +++---- .../SkinEditor/SkinSelectionHandler.cs | 7 ++--- .../Edit/Components/Menus/EditorMenuBar.cs | 27 +---------------- .../Components/Menus/EditorMenuItemSpacer.cs | 13 -------- osu.Game/Screens/Edit/Editor.cs | 10 +++---- 8 files changed, 67 insertions(+), 65 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/OsuMenuItemSpacer.cs delete mode 100644 osu.Game/Screens/Edit/Components/Menus/EditorMenuItemSpacer.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs index dbcf66f005..fe47f5885d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorMenuBar.cs @@ -34,51 +34,51 @@ namespace osu.Game.Tests.Visual.Editing { new MenuItem("File") { - Items = new[] + Items = new OsuMenuItem[] { new EditorMenuItem("Clear All Notes"), new EditorMenuItem("Open Difficulty..."), new EditorMenuItem("Save"), new EditorMenuItem("Create a new Difficulty..."), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Revert to Saved"), new EditorMenuItem("Revert to Saved (Full)"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Test Beatmap"), new EditorMenuItem("Open AiMod"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Upload Beatmap..."), new EditorMenuItem("Export Package"), new EditorMenuItem("Export Map Package"), new EditorMenuItem("Import from..."), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Open Song Folder"), new EditorMenuItem("Open .osu in Notepad"), new EditorMenuItem("Open .osb in Notepad"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Exit"), } }, new MenuItem("Timing") { - Items = new[] + Items = new OsuMenuItem[] { new EditorMenuItem("Time Signature"), new EditorMenuItem("Metronome Clicks"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Add Timing Section"), new EditorMenuItem("Add Inheriting Section"), new EditorMenuItem("Reset Current Section"), new EditorMenuItem("Delete Timing Section"), new EditorMenuItem("Resnap Current Section"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Timing Setup"), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Resnap All Notes", MenuItemType.Destructive), new EditorMenuItem("Move all notes in time...", MenuItemType.Destructive), new EditorMenuItem("Recalculate Slider Lengths", MenuItemType.Destructive), new EditorMenuItem("Delete All Timing Sections", MenuItemType.Destructive), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem("Set Current Position as Preview Point"), } }, diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 73d57af793..e2aac297e3 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -6,13 +6,15 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { @@ -78,6 +80,9 @@ namespace osu.Game.Graphics.UserInterface { case StatefulMenuItem stateful: return new DrawableStatefulMenuItem(stateful); + + case OsuMenuItemSpacer spacer: + return new DrawableSpacer(spacer); } return new DrawableOsuMenuItem(item); @@ -89,5 +94,28 @@ namespace osu.Game.Graphics.UserInterface { Anchor = Direction == Direction.Horizontal ? Anchor.BottomLeft : Anchor.TopRight }; + + protected partial class DrawableSpacer : DrawableOsuMenuItem + { + public DrawableSpacer(MenuItem item) + : base(item) + { + Scale = new Vector2(1, 0.6f); + + AddInternal(new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = BackgroundColourHover, + RelativeSizeAxes = Axes.X, + Height = 2f, + Width = 0.8f, + }); + } + + protected override bool OnHover(HoverEvent e) => true; + + protected override bool OnClick(ClickEvent e) => true; + } } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenuItemSpacer.cs b/osu.Game/Graphics/UserInterface/OsuMenuItemSpacer.cs new file mode 100644 index 0000000000..8a3a928c60 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuMenuItemSpacer.cs @@ -0,0 +1,13 @@ +// 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.UserInterface +{ + public class OsuMenuItemSpacer : OsuMenuItem + { + public OsuMenuItemSpacer() + : base(" ") + { + } + } +} diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index 38eed55241..a816031668 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -151,23 +151,23 @@ namespace osu.Game.Overlays.SkinEditor { new MenuItem(CommonStrings.MenuBarFile) { - Items = new[] + Items = new OsuMenuItem[] { new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } }, - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()), }, }, new MenuItem(CommonStrings.MenuBarEdit) { - Items = new[] + Items = new OsuMenuItem[] { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new EditorMenuItemSpacer(), + 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), diff --git a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs index 52c012a15a..cf6fb60636 100644 --- a/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs +++ b/osu.Game/Overlays/SkinEditor/SkinSelectionHandler.cs @@ -14,7 +14,6 @@ using osu.Framework.Utils; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; -using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Skinning; using osu.Game.Utils; @@ -249,7 +248,7 @@ namespace osu.Game.Overlays.SkinEditor Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray() }; - yield return new EditorMenuItemSpacer(); + yield return new OsuMenuItemSpacer(); yield return new OsuMenuItem("Reset position", MenuItemType.Standard, () => { @@ -277,13 +276,13 @@ namespace osu.Game.Overlays.SkinEditor } }); - yield return new EditorMenuItemSpacer(); + yield return new OsuMenuItemSpacer(); yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront()); yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack()); - yield return new EditorMenuItemSpacer(); + yield return new OsuMenuItemSpacer(); foreach (var item in base.GetContextMenuItemsForSelection(selection)) yield return item; diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index fb0ae2df73..a67375b0a4 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -4,11 +4,9 @@ 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.Textures; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; @@ -151,7 +149,7 @@ namespace osu.Game.Screens.Edit.Components.Menus { switch (item) { - case EditorMenuItemSpacer spacer: + case OsuMenuItemSpacer spacer: return new DrawableSpacer(spacer); case StatefulMenuItem stateful: @@ -195,29 +193,6 @@ namespace osu.Game.Screens.Edit.Components.Menus Foreground.Padding = new MarginPadding { Vertical = 2 }; } } - - private partial class DrawableSpacer : DrawableOsuMenuItem - { - public DrawableSpacer(MenuItem item) - : base(item) - { - Scale = new Vector2(1, 0.6f); - - AddInternal(new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = BackgroundColourHover, - RelativeSizeAxes = Axes.X, - Height = 2f, - Width = 0.8f, - }); - } - - protected override bool OnHover(HoverEvent e) => true; - - protected override bool OnClick(ClickEvent e) => true; - } } } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuItemSpacer.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuItemSpacer.cs deleted file mode 100644 index 4e75a92e19..0000000000 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuItemSpacer.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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.Screens.Edit.Components.Menus -{ - public class EditorMenuItemSpacer : EditorMenuItem - { - public EditorMenuItemSpacer() - : base(" ") - { - } - } -} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3136faf855..5d0cc64cc3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -321,7 +321,7 @@ namespace osu.Game.Screens.Edit { undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), - new EditorMenuItemSpacer(), + 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), @@ -1005,12 +1005,12 @@ namespace osu.Game.Screens.Edit { createDifficultyCreationMenu(), createDifficultySwitchMenu(), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } }, - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()), createExportMenu(), - new EditorMenuItemSpacer(), + new OsuMenuItemSpacer(), new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit) }; @@ -1130,7 +1130,7 @@ namespace osu.Game.Screens.Edit foreach (var rulesetBeatmaps in groupedOrderedBeatmaps) { if (difficultyItems.Count > 0) - difficultyItems.Add(new EditorMenuItemSpacer()); + difficultyItems.Add(new OsuMenuItemSpacer()); foreach (var beatmap in rulesetBeatmaps) { From 9718a802490519780c3d88f55a90037a20b26421 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:24:19 +0900 Subject: [PATCH 791/896] Add visible spacer between "inherit" and other curve types --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f891d23bbd..87e837cc71 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -369,6 +369,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (!selectedPieces.Contains(Pieces[0])) curveTypeItems.Add(createMenuItemForPathType(null)); + curveTypeItems.Add(new OsuMenuItemSpacer()); + // todo: hide/disable items which aren't valid for selected points curveTypeItems.Add(createMenuItemForPathType(PathType.Linear)); curveTypeItems.Add(createMenuItemForPathType(PathType.PerfectCurve)); From c9ee29028fbcf3056c82d5b5c182f862f68974fa Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Tue, 21 Nov 2023 16:54:20 +1100 Subject: [PATCH 792/896] Fix implicitly used method being named incorrectly --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 24d5635104..83538a2f42 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate); yield return (ATTRIB_ID_DIFFICULTY, StarRating); - if (ShouldSerializeFlashlightRating()) + if (ShouldSerializeFlashlightDifficulty()) yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // unless the fields are also renamed. [UsedImplicitly] - public bool ShouldSerializeFlashlightRating() => Mods.Any(m => m is ModFlashlight); + public bool ShouldSerializeFlashlightDifficulty() => Mods.Any(m => m is ModFlashlight); #endregion } From 3afaafb1d9a19692cbd6750eda24cbd7d920c7d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 15:05:51 +0900 Subject: [PATCH 793/896] Reorder and simplify private helper methods --- .../TestSceneOpenEditorTimestampInMania.cs | 53 +++++----- .../TestSceneOpenEditorTimestampInOsu.cs | 65 ++++++------ .../Editing/TestSceneOpenEditorTimestamp.cs | 100 +++++++++--------- 3 files changed, 103 insertions(+), 115 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs index 3c6a9f3b42..05c881d284 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneOpenEditorTimestampInMania.cs @@ -14,35 +14,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor { protected override Ruleset CreateEditorRuleset() => new ManiaRuleset(); - private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true) - { - AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp)); - AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); - } - - private void addReset() - { - addStepClickLink("00:00:000", "reset", false); - } - - private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null) - { - bool checkColumns = columnPairs != null - ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) - : !EditorBeatmap.SelectedHitObjects.Any(); - - return EditorClock.CurrentTime == startTime - && EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0) - && checkColumns; - } - - private bool isNoteAt(HitObject hitObject, double time, int column) - { - return hitObject is ManiaHitObject maniaHitObject - && maniaHitObject.StartTime == time - && maniaHitObject.Column == column; - } - [Test] public void TestNormalSelection() { @@ -95,5 +66,29 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor addStepClickLink("00:00:000 (1,2)", "std link"); AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170)); } + + private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true) + { + AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp)); + AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); + } + + private void addReset() => addStepClickLink("00:00:000", "reset", false); + + private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null) + { + bool checkColumns = columnPairs != null + ? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2))) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime + && EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0) + && checkColumns; + } + + private bool isNoteAt(HitObject hitObject, double time, int column) => + hitObject is ManiaHitObject maniaHitObject + && maniaHitObject.StartTime == time + && maniaHitObject.Column == column; } } diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs index 753400e10e..943858652c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOpenEditorTimestampInOsu.cs @@ -15,40 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); - private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true) - { - AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp)); - AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); - } - - private void addReset() - { - addStepClickLink("00:00:000", "reset", false); - } - - private void checkSelection(Func startTime, params int[] comboNumbers) - => AddUntilStep($"seeked & selected {(comboNumbers.Any() ? string.Join(",", comboNumbers) : "nothing")}", () => - { - bool checkCombos = comboNumbers.Any() - ? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers) - : !EditorBeatmap.SelectedHitObjects.Any(); - - return EditorClock.CurrentTime == startTime() - && EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length - && checkCombos; - }); - - private bool hasCombosInOrder(IEnumerable selected, params int[] comboNumbers) - { - List hitObjects = selected.ToList(); - if (hitObjects.Count != comboNumbers.Length) - return false; - - return !hitObjects.Select(x => (OsuHitObject)x) - .Where((x, i) => x.IndexInCurrentCombo + 1 != comboNumbers[i]) - .Any(); - } - [Test] public void TestNormalSelection() { @@ -90,5 +56,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor addStepClickLink("00:00:956 (956|1,956|2)", "mania link"); checkSelection(() => firstObject.StartTime); } + + private void addReset() => addStepClickLink("00:00:000", "reset", false); + + private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true) + { + AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp)); + AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value); + } + + private void checkSelection(Func startTime, params int[] comboNumbers) + => AddUntilStep($"seeked & selected {(comboNumbers.Any() ? string.Join(",", comboNumbers) : "nothing")}", () => + { + bool checkCombos = comboNumbers.Any() + ? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers) + : !EditorBeatmap.SelectedHitObjects.Any(); + + return EditorClock.CurrentTime == startTime() + && EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length + && checkCombos; + }); + + private bool hasCombosInOrder(IEnumerable selected, params int[] comboNumbers) + { + List hitObjects = selected.ToList(); + if (hitObjects.Count != comboNumbers.Length) + return false; + + return !hitObjects.Select(x => (OsuHitObject)x) + .Where((x, i) => x.IndexInCurrentCombo + 1 != comboNumbers[i]) + .Any(); + } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index 86ceb73f7a..dacc5887d0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -25,58 +25,6 @@ namespace osu.Game.Tests.Visual.Editing private EditorBeatmap editorBeatmap => editor.ChildrenOfType().Single(); private EditorClock editorClock => editor.ChildrenOfType().Single(); - private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true) - { - AddStep($"{step} {timestamp}", () => - Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp)) - ); - - if (waitForSeek) - AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value); - } - - private void addStepScreenModeTo(EditorScreenMode screenMode) - { - AddStep("change screen to " + screenMode, () => editor.Mode.Value = screenMode); - } - - private void assertOnScreenAt(EditorScreenMode screen, double time, string text = "stayed in") - { - AddAssert($"{text} {screen} at {time}", () => - editor.Mode.Value == screen - && editorClock.CurrentTime == time - ); - } - - private void assertMovedScreenTo(EditorScreenMode screen, string text = "moved to") - { - AddAssert($"{text} {screen}", () => editor.Mode.Value == screen); - } - - private void setUpEditor(RulesetInfo ruleset) - { - BeatmapSetInfo beatmapSet = null!; - - AddStep("Import test beatmap", () => - Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely() - ); - AddStep("Retrieve beatmap", () => - beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach() - ); - AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet)); - AddUntilStep("Wait for song select", () => - Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) - && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect - && songSelect.IsLoaded - ); - AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset); - AddStep("Open editor for ruleset", () => - ((PlaySongSelect)Game.ScreenStack.CurrentScreen) - .Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name)) - ); - AddUntilStep("Wait for editor open", () => editor.ReadyForUse); - } - [Test] public void TestErrorNotifications() { @@ -151,5 +99,53 @@ namespace osu.Game.Tests.Visual.Editing addStepClickLink("00:00:000"); assertOnScreenAt(EditorScreenMode.Compose, 0); } + + private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true) + { + AddStep($"{step} {timestamp}", () => + Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp)) + ); + + if (waitForSeek) + AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value); + } + + private void addStepScreenModeTo(EditorScreenMode screenMode) => + AddStep("change screen to " + screenMode, () => editor.Mode.Value = screenMode); + + private void assertOnScreenAt(EditorScreenMode screen, double time) + { + AddAssert($"stayed on {screen} at {time}", () => + editor.Mode.Value == screen + && editorClock.CurrentTime == time + ); + } + + private void assertMovedScreenTo(EditorScreenMode screen, string text = "moved to") => + AddAssert($"{text} {screen}", () => editor.Mode.Value == screen); + + private void setUpEditor(RulesetInfo ruleset) + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("Import test beatmap", () => + Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely() + ); + AddStep("Retrieve beatmap", () => + beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach() + ); + AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("Wait for song select", () => + Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded + ); + AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset); + AddStep("Open editor for ruleset", () => + ((PlaySongSelect)Game.ScreenStack.CurrentScreen) + .Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name)) + ); + AddUntilStep("Wait for editor open", () => editor.ReadyForUse); + } } } From 917a68eac3273f969a7b760cd7a258e475fcc23e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 15:08:15 +0900 Subject: [PATCH 794/896] Adjust localisablel strings and keys --- .../Visual/Editing/TestSceneOpenEditorTimestamp.cs | 8 ++++---- osu.Game/Localisation/EditorStrings.cs | 6 +++--- osu.Game/OsuGame.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index dacc5887d0..b6f89ee4e7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Editing addStepClickLink("00:00:000", waitForSeek: false); AddAssert("received 'must be in edit'", - () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit), + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks), () => Is.EqualTo(1)); AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); @@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Editing addStepClickLink("00:00:000 (1)", waitForSeek: false); AddAssert("received 'must be in edit'", - () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit), + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks), () => Is.EqualTo(2)); setUpEditor(rulesetInfo); @@ -48,12 +48,12 @@ namespace osu.Game.Tests.Visual.Editing addStepClickLink("00:000", "invalid link", waitForSeek: false); AddAssert("received 'failed to process'", - () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp), + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink), () => Is.EqualTo(1)); addStepClickLink("50000:00:000", "too long link", waitForSeek: false); AddAssert("received 'failed to process'", - () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp), + () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink), () => Is.EqualTo(2)); } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index e762455e8b..b20b5bc65a 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -122,12 +122,12 @@ namespace osu.Game.Localisation /// /// "Must be in edit mode to handle editor links" /// - public static LocalisableString MustBeInEdit => new TranslatableString(getKey(@"must_be_in_edit"), @"Must be in edit mode to handle editor links"); + public static LocalisableString MustBeInEditorToHandleLinks => new TranslatableString(getKey(@"must_be_in_editor_to_handle_links"), @"Must be in edit mode to handle editor links"); /// - /// "Failed to process timestamp" + /// "Failed to parse editor link" /// - public static LocalisableString FailedToProcessTimestamp => new TranslatableString(getKey(@"failed_to_process_timestamp"), @"Failed to process timestamp"); + public static LocalisableString FailedToParseEditorLink => new TranslatableString(getKey(@"failed_to_parse_edtior_link"), @"Failed to parse editor link"); private static string getKey(string key) => $@"{prefix}:{key}"; } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3fda18b7cb..80a9853965 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -565,7 +565,7 @@ namespace osu.Game Schedule(() => Notifications.Post(new SimpleNotification { Icon = FontAwesome.Solid.ExclamationTriangle, - Text = EditorStrings.MustBeInEdit + Text = EditorStrings.MustBeInEditorToHandleLinks })); return; } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index fb34767315..7dd2666a7e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1146,7 +1146,7 @@ namespace osu.Game.Screens.Edit Schedule(() => notifications?.Post(new SimpleNotification { Icon = FontAwesome.Solid.ExclamationTriangle, - Text = EditorStrings.FailedToProcessTimestamp + Text = EditorStrings.FailedToParseEditorLink })); return; } From 7c5345bf7e88ba364cd3606e424f4ae45b6760f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 15:09:33 +0900 Subject: [PATCH 795/896] Use `SimpleErrorNotification` for error display --- osu.Game/OsuGame.cs | 2 +- osu.Game/Screens/Edit/Editor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 80a9853965..c4f93636e9 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -562,7 +562,7 @@ namespace osu.Game { if (ScreenStack.CurrentScreen is not Editor editor) { - Schedule(() => Notifications.Post(new SimpleNotification + Schedule(() => Notifications.Post(new SimpleErrorNotification { Icon = FontAwesome.Solid.ExclamationTriangle, Text = EditorStrings.MustBeInEditorToHandleLinks diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7dd2666a7e..b3cebb41de 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -1143,7 +1143,7 @@ namespace osu.Game.Screens.Edit { if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection)) { - Schedule(() => notifications?.Post(new SimpleNotification + Schedule(() => notifications?.Post(new SimpleErrorNotification { Icon = FontAwesome.Solid.ExclamationTriangle, Text = EditorStrings.FailedToParseEditorLink From 314a7bf6f1058195ecf5995fc18a5e5bc1eb94fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 11:33:05 +0900 Subject: [PATCH 796/896] Simplify `AddOnce` call to avoid `self` argument --- .../Sliders/SliderPlacementBlueprint.cs | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 5f072eb171..32fb7ad351 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -3,10 +3,12 @@ #nullable disable +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; @@ -84,16 +86,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { freehandToolboxGroup.Tolerance.BindValueChanged(e => { - if (bSplineBuilder.Tolerance != e.NewValue) - bSplineBuilder.Tolerance = e.NewValue; - updateSliderPathFromBSplineBuilder(); + bSplineBuilder.Tolerance = e.NewValue; + Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); }, true); freehandToolboxGroup.CornerThreshold.BindValueChanged(e => { - if (bSplineBuilder.CornerThreshold != e.NewValue) - bSplineBuilder.CornerThreshold = e.NewValue; - updateSliderPathFromBSplineBuilder(); + bSplineBuilder.CornerThreshold = e.NewValue; + Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); }, true); } } @@ -197,27 +197,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.OnDrag(e); bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position); - updateSliderPathFromBSplineBuilder(); + Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); } private void updateSliderPathFromBSplineBuilder() { - Scheduler.AddOnce(static self => - { - var cps = self.bSplineBuilder.ControlPoints; - var sliderCps = self.HitObject.Path.ControlPoints; - sliderCps.RemoveRange(1, sliderCps.Count - 1); + IReadOnlyList builderPoints = bSplineBuilder.ControlPoints; + BindableList sliderPoints = HitObject.Path.ControlPoints; - // Add the control points from the BSpline builder while converting control points that repeat - // three or more times to a single PathControlPoint with linear type. - for (int i = 1; i < cps.Count; i++) - { - bool isSharp = i < cps.Count - 2 && cps[i] == cps[i + 1] && cps[i] == cps[i + 2]; - sliderCps.Add(new PathControlPoint(cps[i], isSharp ? PathType.BSpline(3) : null)); - if (isSharp) - i += 2; - } - }, this); + sliderPoints.RemoveRange(1, sliderPoints.Count - 1); + + // Add the control points from the BSpline builder while converting control points that repeat + // three or more times to a single PathControlPoint with linear type. + for (int i = 1; i < builderPoints.Count; i++) + { + bool isSharp = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2]; + sliderPoints.Add(new PathControlPoint(builderPoints[i], isSharp ? PathType.BSpline(3) : null)); + if (isSharp) i += 2; + } } protected override void OnDragEnd(DragEndEvent e) From 0a5444d091de73c2cc9fe8360146248d1b06b39e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 13:05:31 +0900 Subject: [PATCH 797/896] Fix using the incorrect position for the first point --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 32fb7ad351..68f5b2d70f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return false; bSplineBuilder.Clear(); - bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position); + bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); setState(SliderPlacementState.Drawing); return true; } From e69e78ad4123a4d89915aa44286a8e637e49429d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 13:10:44 +0900 Subject: [PATCH 798/896] Refactor b-spline path conversion code to better handle linear segments --- .../Sliders/SliderPlacementBlueprint.cs | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 68f5b2d70f..6e9cc26af4 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -8,7 +8,6 @@ using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; @@ -203,17 +202,44 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSliderPathFromBSplineBuilder() { IReadOnlyList builderPoints = bSplineBuilder.ControlPoints; - BindableList sliderPoints = HitObject.Path.ControlPoints; - sliderPoints.RemoveRange(1, sliderPoints.Count - 1); + if (builderPoints.Count == 0) + return; - // Add the control points from the BSpline builder while converting control points that repeat - // three or more times to a single PathControlPoint with linear type. - for (int i = 1; i < builderPoints.Count; i++) + int lastSegmentStart = 0; + PathType? lastPathType = null; + + HitObject.Path.ControlPoints.Clear(); + + // Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate. + // Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started. + for (int i = 0; i < builderPoints.Count; i++) { - bool isSharp = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2]; - sliderPoints.Add(new PathControlPoint(builderPoints[i], isSharp ? PathType.BSpline(3) : null)); - if (isSharp) i += 2; + bool isLastPoint = i == builderPoints.Count - 1; + bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2]; + + if (isNewSegment || isLastPoint) + { + int pointsInSegment = i - lastSegmentStart; + + // Where possible, we can use the simpler LINEAR path type. + PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3); + + // Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined. + if (lastPathType == pathType && lastPathType == PathType.LINEAR) + pathType = null; + + HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType)); + for (int j = lastSegmentStart + 1; j < i; j++) + HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j])); + + if (isLastPoint) + HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i])); + + // Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type. + lastSegmentStart = (i += 2); + if (pathType != null) lastPathType = pathType; + } } } From 5175464c18dafc3931b723ae06dbb4375c7c0a77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 13:35:04 +0900 Subject: [PATCH 799/896] Update test coverage (and add test coverage of curve drawing) --- .../TestSceneSliderPlacementBlueprint.cs | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index c7a21ba689..8498263138 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Rulesets.Edit; @@ -274,7 +275,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor } [Test] - public void TestSliderDrawing() + public void TestSliderDrawingCurve() + { + Vector2 startPoint = new Vector2(200); + + addMovementStep(startPoint); + AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); + + for (int i = 0; i < 20; i++) + addMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50)); + + AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left)); + + assertPlaced(true); + assertLength(760, tolerance: 10); + assertControlPointCount(5); + assertControlPointType(0, PathType.BSpline(3)); + assertControlPointType(1, null); + assertControlPointType(2, null); + assertControlPointType(3, null); + assertControlPointType(4, null); + } + + [Test] + public void TestSliderDrawingLinear() { addMovementStep(new Vector2(200)); AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); @@ -291,7 +315,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertPlaced(true); assertLength(600, tolerance: 10); assertControlPointCount(4); - assertControlPointType(0, PathType.BSpline(3)); + assertControlPointType(0, PathType.LINEAR); + assertControlPointType(1, null); + assertControlPointType(2, null); + assertControlPointType(3, null); } [Test] @@ -401,11 +428,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected); - private void assertLength(double expected, double tolerance = 1) => AddAssert($"slider length is {expected}±{tolerance}", () => Precision.AlmostEquals(expected, getSlider().Distance, tolerance)); + private void assertLength(double expected, double tolerance = 1) => AddAssert($"slider length is {expected}±{tolerance}", () => getSlider().Distance, () => Is.EqualTo(expected).Within(tolerance)); - private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected); + private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count, () => Is.EqualTo(expected)); - private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type == type); + private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider().Path.ControlPoints[index].Type, () => Is.EqualTo(type)); private void assertControlPointPosition(int index, Vector2 position) => AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position, 1)); From 7bedbe42642384a20b681220b507fcc737325c1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 13:36:06 +0900 Subject: [PATCH 800/896] Apply NRT to `SliderPlacementBlueprint` tests --- .../Editor/TestSceneSliderPlacementBlueprint.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 8498263138..75778b3a7e 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.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; using NUnit.Framework; using osu.Framework.Utils; @@ -428,16 +426,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected); - private void assertLength(double expected, double tolerance = 1) => AddAssert($"slider length is {expected}±{tolerance}", () => getSlider().Distance, () => Is.EqualTo(expected).Within(tolerance)); + private void assertLength(double expected, double tolerance = 1) => AddAssert($"slider length is {expected}±{tolerance}", () => getSlider()!.Distance, () => Is.EqualTo(expected).Within(tolerance)); - private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count, () => Is.EqualTo(expected)); + private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider()!.Path.ControlPoints.Count, () => Is.EqualTo(expected)); - private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider().Path.ControlPoints[index].Type, () => Is.EqualTo(type)); + private void assertControlPointType(int index, PathType? type) => AddAssert($"control point {index} is {type?.ToString() ?? "inherit"}", () => getSlider()!.Path.ControlPoints[index].Type, () => Is.EqualTo(type)); private void assertControlPointPosition(int index, Vector2 position) => - AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position, 1)); + AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider()!.Path.ControlPoints[index].Position, 1)); - private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null; + 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(); From 9c3f9db31838295cccc0ea4f771933dbb4fa2dc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:02:08 +0900 Subject: [PATCH 801/896] Add failing test coverage of BSpline encoding parse failure --- .../Formats/LegacyBeatmapEncoderTest.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 0dd277dc89..e847b61fbe 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -113,6 +113,33 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration)); } + [Test] + public void TestEncodeBSplineCurveType() + { + var beatmap = new Beatmap + { + HitObjects = + { + new Slider + { + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero, PathType.BSpline(3)), + new PathControlPoint(new Vector2(50)), + new PathControlPoint(new Vector2(100), PathType.BSpline(3)), + new PathControlPoint(new Vector2(150)) + }) + }, + } + }; + + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((beatmap, new TestLegacySkin(beatmaps_resource_store, string.Empty))), string.Empty); + var decodedSlider = (Slider)decodedAfterEncode.beatmap.HitObjects[0]; + Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(4)); + Assert.That(decodedSlider.Path.ControlPoints[0].Type, Is.EqualTo(PathType.BSpline(3))); + Assert.That(decodedSlider.Path.ControlPoints[2].Type, Is.EqualTo(PathType.BSpline(3))); + } + [Test] public void TestEncodeMultiSegmentSliderWithFloatingPointError() { From 3d094f84add9f669b2bf0e1bd41bd83da4fd01fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:02:15 +0900 Subject: [PATCH 802/896] Fix incorrect parsing of BSpline curve types --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 411a9b0d63..f9e32fe26f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -273,8 +273,8 @@ namespace osu.Game.Rulesets.Objects.Legacy while (++endIndex < pointSplit.Length) { - // Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1). - if (pointSplit[endIndex].Length > 1) + // Keep incrementing endIndex while it's not the start of a new segment (indicated by having an alpha character at position 0). + if (!char.IsLetter(pointSplit[endIndex][0])) continue; // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment. From ba6fbbe43c122413c4b4b81eb9450e77f3361f8b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:03:45 +0900 Subject: [PATCH 803/896] 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 8175869405..aa993485f3 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 a5425ba4c7..b43cb1b3f1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 92728ea56460dfe0c2135d9165ee8d35e49fe8ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:10:48 +0900 Subject: [PATCH 804/896] Simplify toolbox initialisation --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 061f72d72f..65acdb61ae 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -97,12 +97,12 @@ namespace osu.Game.Rulesets.Osu.Edit // we may be entering the screen with a selection already active updateDistanceSnapGrid(); - RightToolbox.Add(new TransformToolboxGroup - { - RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, - }); - - RightToolbox.Add(FreehandlSliderToolboxGroup); + RightToolbox.AddRange(new EditorToolboxGroup[] + { + new TransformToolboxGroup { RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler, }, + FreehandlSliderToolboxGroup + } + ); } protected override ComposeBlueprintContainer CreateBlueprintContainer() From cf6f66b84f729a71e04a34b7d9acf9091dfbd7cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:37:06 +0900 Subject: [PATCH 805/896] Remove redundant `Clear()` call --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 6e9cc26af4..32397330c6 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -178,7 +178,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (lastCp != cursor && HitObject.Path.ControlPoints.Count == 2) return false; - bSplineBuilder.Clear(); bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); setState(SliderPlacementState.Drawing); return true; From 016de7be6a9c0a5709e966b6f2bb45349d8f38e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:51:09 +0900 Subject: [PATCH 806/896] Simplify drag handling code in `SliderPlacementBlueprint` --- .../Sliders/SliderPlacementBlueprint.cs | 47 +++++++------------ 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 32397330c6..bb4558171f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders controlPointVisualiser = new PathControlPointVisualiser(HitObject, false) }; - setState(SliderPlacementState.Initial); + state = SliderPlacementState.Initial; } protected override void LoadComplete() @@ -164,38 +164,30 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override bool OnDragStart(DragStartEvent e) { - if (e.Button == MouseButton.Left) - { - switch (state) - { - case SliderPlacementState.Initial: - return true; + if (e.Button != MouseButton.Left) + return base.OnDragStart(e); - case SliderPlacementState.ControlPoints: - if (HitObject.Path.ControlPoints.Count < 3) - { - var lastCp = HitObject.Path.ControlPoints.LastOrDefault(); - if (lastCp != cursor && HitObject.Path.ControlPoints.Count == 2) - return false; + if (state != SliderPlacementState.ControlPoints) + return base.OnDragStart(e); - bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); - setState(SliderPlacementState.Drawing); - return true; - } + // Only enter drawing mode if no additional control points have been placed. + if (HitObject.Path.ControlPoints.Count > 2) + return base.OnDragStart(e); - return false; - } - } - - return base.OnDragStart(e); + bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); + state = SliderPlacementState.Drawing; + return true; } protected override void OnDrag(DragEvent e) { base.OnDrag(e); - bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position); - Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); + if (state == SliderPlacementState.Drawing) + { + bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position); + Scheduler.AddOnce(updateSliderPathFromBSplineBuilder); + } } private void updateSliderPathFromBSplineBuilder() @@ -260,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void beginCurve() { BeginPlacement(commitStart: true); - setState(SliderPlacementState.ControlPoints); + state = SliderPlacementState.ControlPoints; } private void endCurve() @@ -357,11 +349,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders tailCirclePiece.UpdateFrom(HitObject.TailCircle); } - private void setState(SliderPlacementState newState) - { - state = newState; - } - private enum SliderPlacementState { Initial, From a210469956ca340bb302f76679c49142e2e0ae69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 14:52:21 +0900 Subject: [PATCH 807/896] Reorder methods --- .../Sliders/SliderPlacementBlueprint.cs | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index bb4558171f..397d6ffb91 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -190,50 +190,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - private void updateSliderPathFromBSplineBuilder() - { - IReadOnlyList builderPoints = bSplineBuilder.ControlPoints; - - if (builderPoints.Count == 0) - return; - - int lastSegmentStart = 0; - PathType? lastPathType = null; - - HitObject.Path.ControlPoints.Clear(); - - // Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate. - // Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started. - for (int i = 0; i < builderPoints.Count; i++) - { - bool isLastPoint = i == builderPoints.Count - 1; - bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2]; - - if (isNewSegment || isLastPoint) - { - int pointsInSegment = i - lastSegmentStart; - - // Where possible, we can use the simpler LINEAR path type. - PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3); - - // Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined. - if (lastPathType == pathType && lastPathType == PathType.LINEAR) - pathType = null; - - HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType)); - for (int j = lastSegmentStart + 1; j < i; j++) - HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j])); - - if (isLastPoint) - HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i])); - - // Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type. - lastSegmentStart = (i += 2); - if (pathType != null) lastPathType = pathType; - } - } - } - protected override void OnDragEnd(DragEndEvent e) { base.OnDragEnd(e); @@ -249,6 +205,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders base.OnMouseUp(e); } + protected override void Update() + { + base.Update(); + updateSlider(); + + // Maintain the path type in case it got defaulted to bezier at some point during the drag. + updatePathType(); + } + private void beginCurve() { BeginPlacement(commitStart: true); @@ -261,15 +226,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders EndPlacement(true); } - protected override void Update() - { - base.Update(); - updateSlider(); - - // Maintain the path type in case it got defaulted to bezier at some point during the drag. - updatePathType(); - } - private void updatePathType() { if (state == SliderPlacementState.Drawing) @@ -349,6 +305,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders tailCirclePiece.UpdateFrom(HitObject.TailCircle); } + private void updateSliderPathFromBSplineBuilder() + { + IReadOnlyList builderPoints = bSplineBuilder.ControlPoints; + + if (builderPoints.Count == 0) + return; + + int lastSegmentStart = 0; + PathType? lastPathType = null; + + HitObject.Path.ControlPoints.Clear(); + + // Iterate through generated points, finding each segment and adding non-inheriting path types where appropriate. + // Importantly, the B-Spline builder returns three Vector2s at the same location when a new segment is to be started. + for (int i = 0; i < builderPoints.Count; i++) + { + bool isLastPoint = i == builderPoints.Count - 1; + bool isNewSegment = i < builderPoints.Count - 2 && builderPoints[i] == builderPoints[i + 1] && builderPoints[i] == builderPoints[i + 2]; + + if (isNewSegment || isLastPoint) + { + int pointsInSegment = i - lastSegmentStart; + + // Where possible, we can use the simpler LINEAR path type. + PathType? pathType = pointsInSegment == 1 ? PathType.LINEAR : PathType.BSpline(3); + + // Linear segments can be combined, as two adjacent linear sections are computationally the same as one with the points combined. + if (lastPathType == pathType && lastPathType == PathType.LINEAR) + pathType = null; + + HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[lastSegmentStart], pathType)); + for (int j = lastSegmentStart + 1; j < i; j++) + HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[j])); + + if (isLastPoint) + HitObject.Path.ControlPoints.Add(new PathControlPoint(builderPoints[i])); + + // Skip the redundant duplicated points (see isNewSegment above) which have been coalesced into a path type. + lastSegmentStart = (i += 2); + if (pathType != null) lastPathType = pathType; + } + } + } + private enum SliderPlacementState { Initial, From 1660eb3c15b1075e93a64a2938e8e86451f5df84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 15:31:24 +0900 Subject: [PATCH 808/896] Add failing test coverage of drag after point placement --- .../TestSceneSliderPlacementBlueprint.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 75778b3a7e..7ac34bc6c8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -272,6 +272,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor assertControlPointType(2, PathType.PERFECT_CURVE); } + [Test] + public void TestSliderDrawingDoesntActivateAfterNormalPlacement() + { + Vector2 startPoint = new Vector2(200); + + addMovementStep(startPoint); + addClickStep(MouseButton.Left); + + for (int i = 0; i < 20; i++) + { + if (i == 5) + AddStep("press left button", () => InputManager.PressButton(MouseButton.Left)); + addMovementStep(startPoint + new Vector2(i * 40, MathF.Sin(i * MathF.PI / 5) * 50)); + } + + AddStep("release left button", () => InputManager.ReleaseButton(MouseButton.Left)); + assertPlaced(false); + + addClickStep(MouseButton.Right); + assertPlaced(true); + + assertControlPointType(0, PathType.BEZIER); + } + [Test] public void TestSliderDrawingCurve() { From cc33e121258adb0d607fba5c6bf9897e1979eac5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 15:16:40 +0900 Subject: [PATCH 809/896] Fix dragging after one point already placed incorrectly entering drawing mode --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 397d6ffb91..63370350ed 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -171,7 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders return base.OnDragStart(e); // Only enter drawing mode if no additional control points have been placed. - if (HitObject.Path.ControlPoints.Count > 2) + int controlPointCount = HitObject.Path.ControlPoints.Count; + if (controlPointCount > 2 || (controlPointCount == 2 && HitObject.Path.ControlPoints.Last() != cursor)) return base.OnDragStart(e); bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); From 4b2d8aa6a647b28c13f63dc3047cca04b8b8c398 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 15:31:39 +0900 Subject: [PATCH 810/896] Add `ToString` on `PathType` for better test output --- osu.Game/Rulesets/Objects/Types/PathType.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Types/PathType.cs b/osu.Game/Rulesets/Objects/Types/PathType.cs index f84d43e3e7..23f1ccf0bc 100644 --- a/osu.Game/Rulesets/Objects/Types/PathType.cs +++ b/osu.Game/Rulesets/Objects/Types/PathType.cs @@ -81,5 +81,7 @@ namespace osu.Game.Rulesets.Objects.Types public static bool operator ==(PathType a, PathType b) => a.Equals(b); public static bool operator !=(PathType a, PathType b) => !a.Equals(b); + + public override string ToString() => Description; } } From ed3874682365596f96fac6c8622821786b3645a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 21 Nov 2023 16:14:30 +0900 Subject: [PATCH 811/896] Fix spacer appearing on top of menu --- .../Sliders/Components/PathControlPointVisualiser.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 87e837cc71..3a80d04ab8 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -367,9 +367,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components List curveTypeItems = new List(); if (!selectedPieces.Contains(Pieces[0])) + { curveTypeItems.Add(createMenuItemForPathType(null)); - - curveTypeItems.Add(new OsuMenuItemSpacer()); + curveTypeItems.Add(new OsuMenuItemSpacer()); + } // todo: hide/disable items which aren't valid for selected points curveTypeItems.Add(createMenuItemForPathType(PathType.Linear)); From aa749aeb731dba4743338f88e541c326b8e5c12e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Nov 2023 17:49:56 +0900 Subject: [PATCH 812/896] Save any unsaved changes in the skin editor when game changes screens Closes https://github.com/ppy/osu/issues/25494. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 68d6b7ced5..cbe122395c 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -188,7 +188,10 @@ namespace osu.Game.Overlays.SkinEditor } if (skinEditor.State.Value == Visibility.Visible) + { + skinEditor.Save(false); skinEditor.UpdateTargetScreen(target); + } else { skinEditor.Hide(); From 8302ebcf1ae54b2f0bc3ba7f8ba01b185ffeda39 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 Nov 2023 19:18:02 +0900 Subject: [PATCH 813/896] Remove default DrainRate value --- osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index a8808d08e5..4a651e2650 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The drain rate as a proportion of the total health drained per millisecond. /// - public double DrainRate { get; private set; } = 1; + public double DrainRate { get; private set; } /// /// The beatmap. @@ -139,8 +139,6 @@ namespace osu.Game.Rulesets.Scoring { base.Reset(storeResults); - DrainRate = 1; - if (storeResults) DrainRate = ComputeDrainRate(); From 671177e87120e4cacba35f824950d0847cbeacf6 Mon Sep 17 00:00:00 2001 From: yesseruser Date: Tue, 21 Nov 2023 19:02:23 +0100 Subject: [PATCH 814/896] Renamed UpdateableFlag to ClickableUpdateableFlag. --- osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs | 2 +- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs | 4 ++-- osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs | 4 ++-- osu.Game/Overlays/Rankings/CountryPill.cs | 4 ++-- osu.Game/Overlays/Rankings/Tables/RankingsTable.cs | 2 +- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- osu.Game/Screens/Play/HUD/PlayerFlag.cs | 4 ++-- .../{UpdateableFlag.cs => ClickableUpdateableFlag.cs} | 4 ++-- osu.Game/Users/ExtendedUserPanel.cs | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) rename osu.Game/Users/Drawables/{UpdateableFlag.cs => ClickableUpdateableFlag.cs} (91%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 0bc71924ce..7a7679c376 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("click on flag", () => { - InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); + InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); AddAssert("login overlay is hidden", () => loginOverlay.State.Value == Visibility.Hidden); diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 136c9cc8e7..114ef5db22 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -180,7 +180,7 @@ namespace osu.Game.Online.Leaderboards Masking = true, Children = new Drawable[] { - new UpdateableFlag(user.CountryCode) + new ClickableUpdateableFlag(user.CountryCode) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 1fc997fdad..e144a55a96 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -157,7 +157,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Margin = new MarginPadding { Right = horizontal_inset }, Text = score.DisplayAccuracy, }, - new UpdateableFlag(score.User.CountryCode) + new ClickableUpdateableFlag(score.User.CountryCode) { Size = new Vector2(19, 14), ShowPlaceholderOnUnknown = false, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index 9dc2ce204f..e38e6efd06 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly UpdateableAvatar avatar; private readonly LinkFlowContainer usernameText; private readonly DrawableDate achievedOn; - private readonly UpdateableFlag flag; + private readonly ClickableUpdateableFlag flag; public TopScoreUserSection() { @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, } }, - flag = new UpdateableFlag + flag = new ClickableUpdateableFlag { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 36bd8a5af5..15aaf333f4 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Profile.Header private OsuSpriteText usernameText = null!; private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; - private UpdateableFlag userFlag = null!; + private ClickableUpdateableFlag userFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; @@ -162,7 +162,7 @@ namespace osu.Game.Overlays.Profile.Header Direction = FillDirection.Horizontal, Children = new Drawable[] { - userFlag = new UpdateableFlag + userFlag = new ClickableUpdateableFlag { Size = new Vector2(28, 20), ShowPlaceholderOnUnknown = false, diff --git a/osu.Game/Overlays/Rankings/CountryPill.cs b/osu.Game/Overlays/Rankings/CountryPill.cs index 294b6df34d..bfa7363de8 100644 --- a/osu.Game/Overlays/Rankings/CountryPill.cs +++ b/osu.Game/Overlays/Rankings/CountryPill.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Rankings private readonly Container content; private readonly Box background; - private readonly UpdateableFlag flag; + private readonly ClickableUpdateableFlag flag; private readonly OsuSpriteText countryName; public CountryPill() @@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Rankings Spacing = new Vector2(5, 0), Children = new Drawable[] { - flag = new UpdateableFlag + flag = new ClickableUpdateableFlag { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs index 27d894cdc2..b68ecd709a 100644 --- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Rankings.Tables Margin = new MarginPadding { Bottom = row_spacing }, Children = new[] { - new UpdateableFlag(GetCountryCode(item)) + new ClickableUpdateableFlag(GetCountryCode(item)) { Size = new Vector2(28, 20), ShowPlaceholderOnUnknown = false, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index c79c210e30..1f922073ec 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants FillMode = FillMode.Fit, User = user }, - new UpdateableFlag + new ClickableUpdateableFlag { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs index 85799c03d3..7234db71b5 100644 --- a/osu.Game/Screens/Play/HUD/PlayerFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -12,14 +12,14 @@ namespace osu.Game.Screens.Play.HUD { public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable { - private readonly UpdateableFlag flag; + private readonly ClickableUpdateableFlag flag; private const float default_size = 40f; public PlayerFlag() { Size = new Vector2(default_size, default_size / 1.4f); - InternalChild = flag = new UpdateableFlag + InternalChild = flag = new ClickableUpdateableFlag { RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game/Users/Drawables/UpdateableFlag.cs b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs similarity index 91% rename from osu.Game/Users/Drawables/UpdateableFlag.cs rename to osu.Game/Users/Drawables/ClickableUpdateableFlag.cs index 8f8d7052e5..d19234fc17 100644 --- a/osu.Game/Users/Drawables/UpdateableFlag.cs +++ b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs @@ -11,7 +11,7 @@ using osu.Game.Overlays; namespace osu.Game.Users.Drawables { - public partial class UpdateableFlag : ModelBackedDrawable + public partial class ClickableUpdateableFlag : ModelBackedDrawable { public CountryCode CountryCode { @@ -30,7 +30,7 @@ namespace osu.Game.Users.Drawables /// public Action? Action; - public UpdateableFlag(CountryCode countryCode = CountryCode.Unknown) + public ClickableUpdateableFlag(CountryCode countryCode = CountryCode.Unknown) { CountryCode = countryCode; } diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index 3c1b68f9ef..e798c8cc11 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -52,7 +52,7 @@ namespace osu.Game.Users protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false); - protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.CountryCode) + protected ClickableUpdateableFlag CreateFlag() => new ClickableUpdateableFlag(User.CountryCode) { Size = new Vector2(36, 26), Action = Action, From cd7e0bf620c0fd1617f1d99038dbbb85e52acd39 Mon Sep 17 00:00:00 2001 From: yesseruser Date: Tue, 21 Nov 2023 19:27:33 +0100 Subject: [PATCH 815/896] Created and implemened a BaseUpdateableFlag. The tooltip still shows. --- osu.Game/Screens/Play/HUD/PlayerFlag.cs | 4 +- .../Users/Drawables/BaseUpdateableFlag.cs | 45 +++++++++++++++++++ .../Drawables/ClickableUpdateableFlag.cs | 13 +----- 3 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Users/Drawables/BaseUpdateableFlag.cs diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs index 7234db71b5..f0fe8ad668 100644 --- a/osu.Game/Screens/Play/HUD/PlayerFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -12,14 +12,14 @@ namespace osu.Game.Screens.Play.HUD { public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable { - private readonly ClickableUpdateableFlag flag; + private readonly BaseUpdateableFlag flag; private const float default_size = 40f; public PlayerFlag() { Size = new Vector2(default_size, default_size / 1.4f); - InternalChild = flag = new ClickableUpdateableFlag + InternalChild = flag = new BaseUpdateableFlag { RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game/Users/Drawables/BaseUpdateableFlag.cs b/osu.Game/Users/Drawables/BaseUpdateableFlag.cs new file mode 100644 index 0000000000..16a368c60c --- /dev/null +++ b/osu.Game/Users/Drawables/BaseUpdateableFlag.cs @@ -0,0 +1,45 @@ +// 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; + +namespace osu.Game.Users.Drawables +{ + public partial class BaseUpdateableFlag : ModelBackedDrawable + { + public CountryCode CountryCode + { + get => Model; + set => Model = value; + } + + /// + /// Whether to show a place holder on unknown country. + /// + public bool ShowPlaceholderOnUnknown = true; + + public BaseUpdateableFlag(CountryCode countryCode = CountryCode.Unknown) + { + CountryCode = countryCode; + } + + protected override Drawable? CreateDrawable(CountryCode countryCode) + { + if (countryCode == CountryCode.Unknown && !ShowPlaceholderOnUnknown) + return null; + + return new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new DrawableFlag(countryCode) + { + RelativeSizeAxes = Axes.Both + }, + } + }; + } + } +} diff --git a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs index d19234fc17..e00fe3c009 100644 --- a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs +++ b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs @@ -11,19 +11,8 @@ using osu.Game.Overlays; namespace osu.Game.Users.Drawables { - public partial class ClickableUpdateableFlag : ModelBackedDrawable + public partial class ClickableUpdateableFlag : BaseUpdateableFlag { - public CountryCode CountryCode - { - get => Model; - set => Model = value; - } - - /// - /// Whether to show a place holder on unknown country. - /// - public bool ShowPlaceholderOnUnknown = true; - /// /// Perform an action in addition to showing the country ranking. /// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX). From b2f74325efb4f477b42320cca8cbd380895b12da Mon Sep 17 00:00:00 2001 From: yesseruser Date: Tue, 21 Nov 2023 19:51:35 +0100 Subject: [PATCH 816/896] Added a BaseDrawableFlag without a tooltip, and used it. The BaseDrawableFlag is used in a BaseUpdateableFlag so the tooltip is not shown, the ClickableUpdateableFlag still shows the tooltip. --- osu.Game/Users/Drawables/BaseDrawableFlag.cs | 29 +++++++++++++++++++ .../Users/Drawables/BaseUpdateableFlag.cs | 3 +- .../Drawables/ClickableUpdateableFlag.cs | 1 + osu.Game/Users/Drawables/DrawableFlag.cs | 14 +++------ 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Users/Drawables/BaseDrawableFlag.cs diff --git a/osu.Game/Users/Drawables/BaseDrawableFlag.cs b/osu.Game/Users/Drawables/BaseDrawableFlag.cs new file mode 100644 index 0000000000..90f8cd7b70 --- /dev/null +++ b/osu.Game/Users/Drawables/BaseDrawableFlag.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 System; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Users.Drawables +{ + public partial class BaseDrawableFlag : Sprite + { + protected readonly CountryCode CountryCode; + + public BaseDrawableFlag(CountryCode countryCode) + { + CountryCode = countryCode; + } + + [BackgroundDependencyLoader] + private void load(TextureStore ts) + { + ArgumentNullException.ThrowIfNull(ts); + + string textureName = CountryCode == CountryCode.Unknown ? "__" : CountryCode.ToString(); + Texture = ts.Get($@"Flags/{textureName}") ?? ts.Get(@"Flags/__"); + } + } +} diff --git a/osu.Game/Users/Drawables/BaseUpdateableFlag.cs b/osu.Game/Users/Drawables/BaseUpdateableFlag.cs index 16a368c60c..3a27238b19 100644 --- a/osu.Game/Users/Drawables/BaseUpdateableFlag.cs +++ b/osu.Game/Users/Drawables/BaseUpdateableFlag.cs @@ -34,7 +34,8 @@ namespace osu.Game.Users.Drawables RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new DrawableFlag(countryCode) + // This is a BaseDrawableFlag which does not show a tooltip. + new BaseDrawableFlag(countryCode) { RelativeSizeAxes = Axes.Both }, diff --git a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs index e00fe3c009..2eaa3661d1 100644 --- a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs +++ b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs @@ -34,6 +34,7 @@ namespace osu.Game.Users.Drawables RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + // This is a DrawableFlag which implements IHasTooltip, so a tooltip is shown. new DrawableFlag(countryCode) { RelativeSizeAxes = Axes.Both diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs index 289f68ee7f..aef34c093f 100644 --- a/osu.Game/Users/Drawables/DrawableFlag.cs +++ b/osu.Game/Users/Drawables/DrawableFlag.cs @@ -5,29 +5,23 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; namespace osu.Game.Users.Drawables { - public partial class DrawableFlag : Sprite, IHasTooltip + public partial class DrawableFlag : BaseDrawableFlag, IHasTooltip { - private readonly CountryCode countryCode; + public LocalisableString TooltipText => CountryCode == CountryCode.Unknown ? string.Empty : CountryCode.GetDescription(); - public LocalisableString TooltipText => countryCode == CountryCode.Unknown ? string.Empty : countryCode.GetDescription(); - - public DrawableFlag(CountryCode countryCode) - { - this.countryCode = countryCode; - } + public DrawableFlag(CountryCode countryCode) : base(countryCode) { } [BackgroundDependencyLoader] private void load(TextureStore ts) { ArgumentNullException.ThrowIfNull(ts); - string textureName = countryCode == CountryCode.Unknown ? "__" : countryCode.ToString(); + string textureName = CountryCode == CountryCode.Unknown ? "__" : CountryCode.ToString(); Texture = ts.Get($@"Flags/{textureName}") ?? ts.Get(@"Flags/__"); } } From c98be5823db0818bf5eaf8bcab853fa8622d5257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 Nov 2023 07:52:28 +0900 Subject: [PATCH 817/896] 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 aa993485f3..ea08992710 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 b43cb1b3f1..53d5d6b010 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From aa724070653dc41df9e67c8476427752586b219f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 Nov 2023 07:55:39 +0900 Subject: [PATCH 818/896] Fix android compile failures due to invalid java version See https://github.com/ppy/osu-framework/pull/6057. --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8167ec4db..11c956bb71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,6 +108,12 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Setup JDK 11 + uses: actions/setup-java@v3 + with: + distribution: microsoft + java-version: 11 + - name: Install .NET 6.0.x uses: actions/setup-dotnet@v3 with: From 0cf925dadf4e09fc6e8e8c14f2511f4ef8591a18 Mon Sep 17 00:00:00 2001 From: Poyo Date: Tue, 21 Nov 2023 15:18:04 -0800 Subject: [PATCH 819/896] Use better fallback Seems better to use the rate from a non-gameplay clock than to arbitrarily apply 1. --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5abca168ed..1daaa24d57 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -704,7 +704,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } Result.RawTime = Time.Current; - Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? 1.0; + Result.GameplayRate = (Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate; if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); From 3a0586a8f59ce286f986df00327cf7953fd62dd7 Mon Sep 17 00:00:00 2001 From: Poyo Date: Tue, 21 Nov 2023 15:19:04 -0800 Subject: [PATCH 820/896] Rewrite backwards assertion --- osu.Game/Rulesets/Scoring/HitEventExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs index 70a11ae760..9fb61c6cd9 100644 --- a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Scoring /// public static double? CalculateUnstableRate(this IEnumerable hitEvents) { - Debug.Assert(!hitEvents.Any(ev => ev.GameplayRate == null)); + Debug.Assert(hitEvents.All(ev => ev.GameplayRate != null)); // Division by gameplay rate is to account for TimeOffset scaling with gameplay rate. double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset / ev.GameplayRate!.Value).ToArray(); From 04640b6fb0129aa880281d1883bf0b318a128fe0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 10:44:29 +0900 Subject: [PATCH 821/896] Improve commenting around `IHasCombo` interfaces Following discusion with smoogi IRL. --- osu.Game/Rulesets/Objects/Types/IHasCombo.cs | 6 ++++++ .../Rulesets/Objects/Types/IHasComboInformation.cs | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs index d1a4683a1d..5de5424bdc 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs @@ -16,6 +16,12 @@ namespace osu.Game.Rulesets.Objects.Types /// /// When starting a new combo, the offset of the new combo relative to the current one. /// + /// + /// This is generally a setting provided by a beatmap creator to choreograph interesting colour patterns + /// which can only be achieved by skipping combo colours with per-hitobject level. + /// + /// It is exposed via . + /// int ComboOffset { get; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index d34e71021f..3aa68197ec 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Objects.Types /// public interface IHasComboInformation : IHasCombo { + /// + /// Bindable exposure of . + /// Bindable IndexInCurrentComboBindable { get; } /// @@ -19,13 +22,21 @@ namespace osu.Game.Rulesets.Objects.Types /// int IndexInCurrentCombo { get; set; } + /// + /// Bindable exposure of . + /// Bindable ComboIndexBindable { get; } /// /// The index of this combo in relation to the beatmap. + /// + /// In other words, this is incremented by 1 each time a is reached. /// int ComboIndex { get; set; } + /// + /// Bindable exposure of . + /// Bindable ComboIndexWithOffsetsBindable { get; } /// @@ -39,6 +50,9 @@ namespace osu.Game.Rulesets.Objects.Types /// new bool NewCombo { get; set; } + /// + /// Bindable exposure of . + /// Bindable LastInComboBindable { get; } /// From fe15b26bd2f780ba6e45e81e64944841ec23a54d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 12:02:37 +0900 Subject: [PATCH 822/896] Refactor to use API state instead of logged in user state --- osu.Game/Overlays/UserProfileOverlay.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 193651fa72..d978fe905f 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays private IUser? user; private IRulesetInfo? ruleset; - private IBindable apiUser = null!; + private readonly IBindable apiState = new Bindable(); [Resolved] private RulesetStore rulesets { get; set; } = null!; @@ -68,10 +68,10 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - apiUser = API.LocalUser.GetBoundCopy(); - apiUser.BindValueChanged(_ => Schedule(() => + apiState.BindTo(API.State); + apiState.BindValueChanged(state => Schedule(() => { - if (API.IsLoggedIn) + if (state.NewValue == APIState.Online && user != null) fetchAndSetContent(); })); } @@ -89,7 +89,6 @@ namespace osu.Game.Overlays ruleset = userRuleset; Show(); - fetchAndSetContent(); } @@ -171,13 +170,14 @@ namespace osu.Game.Overlays sectionsContainer.ScrollToTop(); - if (API.State.Value != APIState.Online) - return; + if (API.State.Value != APIState.Offline) + { + userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); + userReq.Success += u => userLoadComplete(u, ruleset); - userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset); - userReq.Success += u => userLoadComplete(u, ruleset); - API.Queue(userReq); - loadingLayer.Show(); + API.Queue(userReq); + loadingLayer.Show(); + } } private void userLoadComplete(APIUser loadedUser, IRulesetInfo? userRuleset) From ad6af1d9b74302b1d0396baba0fad68f31f88441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 12:03:42 +0900 Subject: [PATCH 823/896] Ensure only run once --- osu.Game/Overlays/UserProfileOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index d978fe905f..9840551d9f 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays apiState.BindValueChanged(state => Schedule(() => { if (state.NewValue == APIState.Online && user != null) - fetchAndSetContent(); + Scheduler.AddOnce(fetchAndSetContent); })); } @@ -89,7 +89,7 @@ namespace osu.Game.Overlays ruleset = userRuleset; Show(); - fetchAndSetContent(); + Scheduler.AddOnce(fetchAndSetContent); } private void fetchAndSetContent() From cb4568c4a1e7d12fea7dda4a643878e9c2bae675 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Nov 2023 20:21:11 +0900 Subject: [PATCH 824/896] Fix first object after break not starting a new combo --- .../Formats/LegacyBeatmapDecoderTest.cs | 15 ++++++++++++ .../TestSceneHitObjectAccentColour.cs | 3 --- .../Resources/break-between-objects.osu | 15 ++++++++++++ .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 23 +++++++++++++++++++ .../Objects/Legacy/Catch/ConvertHit.cs | 6 +---- .../Objects/Legacy/Catch/ConvertSlider.cs | 6 +---- .../Objects/Legacy/Catch/ConvertSpinner.cs | 6 +---- .../Objects/Legacy/ConvertHitObject.cs | 7 +++++- .../Rulesets/Objects/Legacy/Osu/ConvertHit.cs | 6 +---- .../Objects/Legacy/Osu/ConvertSlider.cs | 6 +---- .../Objects/Legacy/Osu/ConvertSpinner.cs | 6 +---- 11 files changed, 65 insertions(+), 34 deletions(-) create mode 100644 osu.Game.Tests/Resources/break-between-objects.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 66151a51e6..be1993957f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -1093,5 +1093,20 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(hitObject.Samples.Select(s => s.Volume), Has.All.EqualTo(70)); } } + + [Test] + public void TestNewComboAfterBreak() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("break-between-objects.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var beatmap = decoder.Decode(stream); + Assert.That(((IHasCombo)beatmap.HitObjects[0]).NewCombo, Is.True); + Assert.That(((IHasCombo)beatmap.HitObjects[1]).NewCombo, Is.True); + Assert.That(((IHasCombo)beatmap.HitObjects[2]).NewCombo, Is.False); + } + } } } diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index f38c2c9416..acb14f86fc 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -94,9 +94,6 @@ namespace osu.Game.Tests.Gameplay private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation { - public bool NewCombo { get; set; } - public int ComboOffset => 0; - public Bindable IndexInCurrentComboBindable { get; } = new Bindable(); public int IndexInCurrentCombo diff --git a/osu.Game.Tests/Resources/break-between-objects.osu b/osu.Game.Tests/Resources/break-between-objects.osu new file mode 100644 index 0000000000..91821e2c58 --- /dev/null +++ b/osu.Game.Tests/Resources/break-between-objects.osu @@ -0,0 +1,15 @@ +osu file format v14 + +[General] +Mode: 0 + +[Events] +2,200,1200 + +[TimingPoints] +0,307.692307692308,4,2,1,60,1,0 + +[HitObjects] +142,99,0,1,0,0:0:0:0: +323,88,3000,1,0,0:0:0:0: +323,88,4000,1,0,0:0:0:0: diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 8c5e4971d5..1ee4670ae2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -93,6 +93,8 @@ namespace osu.Game.Beatmaps.Formats // The parsing order of hitobjects matters in mania difficulty calculation this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList(); + postProcessBreaks(this.beatmap); + foreach (var hitObject in this.beatmap.HitObjects) { applyDefaults(hitObject); @@ -100,6 +102,27 @@ namespace osu.Game.Beatmaps.Formats } } + /// + /// Processes the beatmap such that a new combo is started the first hitobject following each break. + /// + private void postProcessBreaks(Beatmap beatmap) + { + int currentBreak = 0; + bool forceNewCombo = false; + + foreach (var h in beatmap.HitObjects.OfType()) + { + while (currentBreak < beatmap.Breaks.Count && beatmap.Breaks[currentBreak].EndTime < h.StartTime) + { + forceNewCombo = true; + currentBreak++; + } + + h.NewCombo |= forceNewCombo; + forceNewCombo = false; + } + } + private void applyDefaults(HitObject hitObject) { DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT; diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs index 12b4812824..96c779e79b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs @@ -9,16 +9,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Hit-type, used for parsing Beatmaps. /// - internal sealed class ConvertHit : ConvertHitObject, IHasPosition, IHasCombo + internal sealed class ConvertHit : ConvertHitObject, IHasPosition { public float X => Position.X; public float Y => Position.Y; public Vector2 Position { get; set; } - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs index fb1afed3b4..bcf1c7fae2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs @@ -9,16 +9,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition { public float X => Position.X; public float Y => Position.Y; public Vector2 Position { get; set; } - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index 014494ec54..5ef3d51cb3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -8,16 +8,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition, IHasCombo + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition { public double EndTime => StartTime + Duration; public double Duration { get; set; } public float X => 256; // Required for CatchBeatmapConverter - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs index 54dbd28c76..bb36aab0b3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObject.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Objects.Legacy @@ -9,8 +10,12 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// A hit object only used for conversion, not actual gameplay. /// - internal abstract class ConvertHitObject : HitObject + internal abstract class ConvertHitObject : HitObject, IHasCombo { + public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } + public override Judgement CreateJudgement() => new IgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs index 069366bad3..b7cd4b0dcc 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -9,16 +9,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Hit-type, used for parsing Beatmaps. /// - internal sealed class ConvertHit : ConvertHitObject, IHasPosition, IHasCombo + internal sealed class ConvertHit : ConvertHitObject, IHasPosition { public Vector2 Position { get; set; } public float X => Position.X; public float Y => Position.Y; - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index 790af6cfc1..8c37154f95 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo, IHasGenerateTicks + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasGenerateTicks { public Vector2 Position { get; set; } @@ -17,10 +17,6 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float Y => Position.Y; - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } - public bool GenerateTicks { get; set; } = true; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index e9e5ca8c94..d6e24b6bbf 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition, IHasCombo + internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition { public double Duration { get; set; } @@ -20,9 +20,5 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float X => Position.X; public float Y => Position.Y; - - public bool NewCombo { get; set; } - - public int ComboOffset { get; set; } } } From f7fce1d714dc4d849aef066abe62d1feb4a3145f Mon Sep 17 00:00:00 2001 From: cs Date: Wed, 22 Nov 2023 09:55:32 +0100 Subject: [PATCH 825/896] Fix freehand-drawn sliders with distance snap --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 63370350ed..c35c6fdf95 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -175,6 +175,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (controlPointCount > 2 || (controlPointCount == 2 && HitObject.Path.ControlPoints.Last() != cursor)) return base.OnDragStart(e); + HitObject.Position = ToLocalSpace(e.ScreenSpaceMouseDownPosition); bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); state = SliderPlacementState.Drawing; return true; From 95b12082aed0bb40e998f8b0402be77cfb4639e1 Mon Sep 17 00:00:00 2001 From: cs Date: Wed, 22 Nov 2023 10:14:44 +0100 Subject: [PATCH 826/896] Update to respect distance snap This will cause freehand drawing to respect distance snap, instead changing the drawn path to start from the sliders start position and "snap" in a linear fashion to the cursor position from the position indicated by distance snap --- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index c35c6fdf95..ac7b5e63e1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders if (controlPointCount > 2 || (controlPointCount == 2 && HitObject.Path.ControlPoints.Last() != cursor)) return base.OnDragStart(e); - HitObject.Position = ToLocalSpace(e.ScreenSpaceMouseDownPosition); + bSplineBuilder.AddLinearPoint(Vector2.Zero); bSplineBuilder.AddLinearPoint(ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position); state = SliderPlacementState.Drawing; return true; From aa8dc6bd8061a1eafa591b00fbefa706559e283a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 21:46:35 +0900 Subject: [PATCH 827/896] Attempt to fix intermittent failures on new tests See https://github.com/ppy/osu/pull/25418/checks?check_run_id=18886372597. --- .../Visual/Editing/TestSceneOpenEditorTimestamp.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index b6f89ee4e7..2c8655a5f5 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Editing RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo; addStepClickLink("00:00:000", waitForSeek: false); - AddAssert("received 'must be in edit'", + AddUntilStep("received 'must be in edit'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks), () => Is.EqualTo(1)); @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); addStepClickLink("00:00:000 (1)", waitForSeek: false); - AddAssert("received 'must be in edit'", + AddUntilStep("received 'must be in edit'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks), () => Is.EqualTo(2)); @@ -47,12 +47,12 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("ruleset is osu!", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo)); addStepClickLink("00:000", "invalid link", waitForSeek: false); - AddAssert("received 'failed to process'", + AddUntilStep("received 'failed to process'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink), () => Is.EqualTo(1)); addStepClickLink("50000:00:000", "too long link", waitForSeek: false); - AddAssert("received 'failed to process'", + AddUntilStep("received 'failed to process'", () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink), () => Is.EqualTo(2)); } From 7bc304f20ee27d7314fc06620cf1250acebba89f Mon Sep 17 00:00:00 2001 From: yesseruser Date: Wed, 22 Nov 2023 15:25:17 +0100 Subject: [PATCH 828/896] Revert "Added a BaseDrawableFlag without a tooltip, and used it." This reverts commit b2f74325efb4f477b42320cca8cbd380895b12da. --- osu.Game/Users/Drawables/BaseDrawableFlag.cs | 29 ------------------- .../Users/Drawables/BaseUpdateableFlag.cs | 3 +- .../Drawables/ClickableUpdateableFlag.cs | 1 - osu.Game/Users/Drawables/DrawableFlag.cs | 14 ++++++--- 4 files changed, 11 insertions(+), 36 deletions(-) delete mode 100644 osu.Game/Users/Drawables/BaseDrawableFlag.cs diff --git a/osu.Game/Users/Drawables/BaseDrawableFlag.cs b/osu.Game/Users/Drawables/BaseDrawableFlag.cs deleted file mode 100644 index 90f8cd7b70..0000000000 --- a/osu.Game/Users/Drawables/BaseDrawableFlag.cs +++ /dev/null @@ -1,29 +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.Graphics.Sprites; -using osu.Framework.Graphics.Textures; - -namespace osu.Game.Users.Drawables -{ - public partial class BaseDrawableFlag : Sprite - { - protected readonly CountryCode CountryCode; - - public BaseDrawableFlag(CountryCode countryCode) - { - CountryCode = countryCode; - } - - [BackgroundDependencyLoader] - private void load(TextureStore ts) - { - ArgumentNullException.ThrowIfNull(ts); - - string textureName = CountryCode == CountryCode.Unknown ? "__" : CountryCode.ToString(); - Texture = ts.Get($@"Flags/{textureName}") ?? ts.Get(@"Flags/__"); - } - } -} diff --git a/osu.Game/Users/Drawables/BaseUpdateableFlag.cs b/osu.Game/Users/Drawables/BaseUpdateableFlag.cs index 3a27238b19..16a368c60c 100644 --- a/osu.Game/Users/Drawables/BaseUpdateableFlag.cs +++ b/osu.Game/Users/Drawables/BaseUpdateableFlag.cs @@ -34,8 +34,7 @@ namespace osu.Game.Users.Drawables RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - // This is a BaseDrawableFlag which does not show a tooltip. - new BaseDrawableFlag(countryCode) + new DrawableFlag(countryCode) { RelativeSizeAxes = Axes.Both }, diff --git a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs index 2eaa3661d1..e00fe3c009 100644 --- a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs +++ b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs @@ -34,7 +34,6 @@ namespace osu.Game.Users.Drawables RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - // This is a DrawableFlag which implements IHasTooltip, so a tooltip is shown. new DrawableFlag(countryCode) { RelativeSizeAxes = Axes.Both diff --git a/osu.Game/Users/Drawables/DrawableFlag.cs b/osu.Game/Users/Drawables/DrawableFlag.cs index aef34c093f..289f68ee7f 100644 --- a/osu.Game/Users/Drawables/DrawableFlag.cs +++ b/osu.Game/Users/Drawables/DrawableFlag.cs @@ -5,23 +5,29 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; namespace osu.Game.Users.Drawables { - public partial class DrawableFlag : BaseDrawableFlag, IHasTooltip + public partial class DrawableFlag : Sprite, IHasTooltip { - public LocalisableString TooltipText => CountryCode == CountryCode.Unknown ? string.Empty : CountryCode.GetDescription(); + private readonly CountryCode countryCode; - public DrawableFlag(CountryCode countryCode) : base(countryCode) { } + public LocalisableString TooltipText => countryCode == CountryCode.Unknown ? string.Empty : countryCode.GetDescription(); + + public DrawableFlag(CountryCode countryCode) + { + this.countryCode = countryCode; + } [BackgroundDependencyLoader] private void load(TextureStore ts) { ArgumentNullException.ThrowIfNull(ts); - string textureName = CountryCode == CountryCode.Unknown ? "__" : CountryCode.ToString(); + string textureName = countryCode == CountryCode.Unknown ? "__" : countryCode.ToString(); Texture = ts.Get($@"Flags/{textureName}") ?? ts.Get(@"Flags/__"); } } From be8b59e59d28ac99620e0252215ad7a4acf82604 Mon Sep 17 00:00:00 2001 From: yesseruser Date: Wed, 22 Nov 2023 15:25:35 +0100 Subject: [PATCH 829/896] Revert "Created and implemened a BaseUpdateableFlag." This reverts commit cd7e0bf620c0fd1617f1d99038dbbb85e52acd39. --- osu.Game/Screens/Play/HUD/PlayerFlag.cs | 4 +- .../Users/Drawables/BaseUpdateableFlag.cs | 45 ------------------- .../Drawables/ClickableUpdateableFlag.cs | 13 +++++- 3 files changed, 14 insertions(+), 48 deletions(-) delete mode 100644 osu.Game/Users/Drawables/BaseUpdateableFlag.cs diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs index f0fe8ad668..7234db71b5 100644 --- a/osu.Game/Screens/Play/HUD/PlayerFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -12,14 +12,14 @@ namespace osu.Game.Screens.Play.HUD { public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable { - private readonly BaseUpdateableFlag flag; + private readonly ClickableUpdateableFlag flag; private const float default_size = 40f; public PlayerFlag() { Size = new Vector2(default_size, default_size / 1.4f); - InternalChild = flag = new BaseUpdateableFlag + InternalChild = flag = new ClickableUpdateableFlag { RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game/Users/Drawables/BaseUpdateableFlag.cs b/osu.Game/Users/Drawables/BaseUpdateableFlag.cs deleted file mode 100644 index 16a368c60c..0000000000 --- a/osu.Game/Users/Drawables/BaseUpdateableFlag.cs +++ /dev/null @@ -1,45 +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; - -namespace osu.Game.Users.Drawables -{ - public partial class BaseUpdateableFlag : ModelBackedDrawable - { - public CountryCode CountryCode - { - get => Model; - set => Model = value; - } - - /// - /// Whether to show a place holder on unknown country. - /// - public bool ShowPlaceholderOnUnknown = true; - - public BaseUpdateableFlag(CountryCode countryCode = CountryCode.Unknown) - { - CountryCode = countryCode; - } - - protected override Drawable? CreateDrawable(CountryCode countryCode) - { - if (countryCode == CountryCode.Unknown && !ShowPlaceholderOnUnknown) - return null; - - return new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new DrawableFlag(countryCode) - { - RelativeSizeAxes = Axes.Both - }, - } - }; - } - } -} diff --git a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs index e00fe3c009..d19234fc17 100644 --- a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs +++ b/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs @@ -11,8 +11,19 @@ using osu.Game.Overlays; namespace osu.Game.Users.Drawables { - public partial class ClickableUpdateableFlag : BaseUpdateableFlag + public partial class ClickableUpdateableFlag : ModelBackedDrawable { + public CountryCode CountryCode + { + get => Model; + set => Model = value; + } + + /// + /// Whether to show a place holder on unknown country. + /// + public bool ShowPlaceholderOnUnknown = true; + /// /// Perform an action in addition to showing the country ranking. /// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX). From 08e0279d72d1f76f099c3cda6f6dd0c086123501 Mon Sep 17 00:00:00 2001 From: yesseruser Date: Wed, 22 Nov 2023 15:25:43 +0100 Subject: [PATCH 830/896] Revert "Renamed UpdateableFlag to ClickableUpdateableFlag." This reverts commit 671177e87120e4cacba35f824950d0847cbeacf6. --- osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs | 2 +- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs | 4 ++-- osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs | 4 ++-- osu.Game/Overlays/Rankings/CountryPill.cs | 4 ++-- osu.Game/Overlays/Rankings/Tables/RankingsTable.cs | 2 +- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- osu.Game/Screens/Play/HUD/PlayerFlag.cs | 4 ++-- .../{ClickableUpdateableFlag.cs => UpdateableFlag.cs} | 4 ++-- osu.Game/Users/ExtendedUserPanel.cs | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) rename osu.Game/Users/Drawables/{ClickableUpdateableFlag.cs => UpdateableFlag.cs} (91%) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs index 7a7679c376..0bc71924ce 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginOverlay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("click on flag", () => { - InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); + InputManager.MoveMouseTo(loginOverlay.ChildrenOfType().First()); InputManager.Click(MouseButton.Left); }); AddAssert("login overlay is hidden", () => loginOverlay.State.Value == Visibility.Hidden); diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 114ef5db22..136c9cc8e7 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -180,7 +180,7 @@ namespace osu.Game.Online.Leaderboards Masking = true, Children = new Drawable[] { - new ClickableUpdateableFlag(user.CountryCode) + new UpdateableFlag(user.CountryCode) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index e144a55a96..1fc997fdad 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -157,7 +157,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Margin = new MarginPadding { Right = horizontal_inset }, Text = score.DisplayAccuracy, }, - new ClickableUpdateableFlag(score.User.CountryCode) + new UpdateableFlag(score.User.CountryCode) { Size = new Vector2(19, 14), ShowPlaceholderOnUnknown = false, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs index e38e6efd06..9dc2ce204f 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly UpdateableAvatar avatar; private readonly LinkFlowContainer usernameText; private readonly DrawableDate achievedOn; - private readonly ClickableUpdateableFlag flag; + private readonly UpdateableFlag flag; public TopScoreUserSection() { @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }, } }, - flag = new ClickableUpdateableFlag + flag = new UpdateableFlag { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 15aaf333f4..36bd8a5af5 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Profile.Header private OsuSpriteText usernameText = null!; private ExternalLinkButton openUserExternally = null!; private OsuSpriteText titleText = null!; - private ClickableUpdateableFlag userFlag = null!; + private UpdateableFlag userFlag = null!; private OsuHoverContainer userCountryContainer = null!; private OsuSpriteText userCountryText = null!; private GroupBadgeFlow groupBadgeFlow = null!; @@ -162,7 +162,7 @@ namespace osu.Game.Overlays.Profile.Header Direction = FillDirection.Horizontal, Children = new Drawable[] { - userFlag = new ClickableUpdateableFlag + userFlag = new UpdateableFlag { Size = new Vector2(28, 20), ShowPlaceholderOnUnknown = false, diff --git a/osu.Game/Overlays/Rankings/CountryPill.cs b/osu.Game/Overlays/Rankings/CountryPill.cs index bfa7363de8..294b6df34d 100644 --- a/osu.Game/Overlays/Rankings/CountryPill.cs +++ b/osu.Game/Overlays/Rankings/CountryPill.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Rankings private readonly Container content; private readonly Box background; - private readonly ClickableUpdateableFlag flag; + private readonly UpdateableFlag flag; private readonly OsuSpriteText countryName; public CountryPill() @@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Rankings Spacing = new Vector2(5, 0), Children = new Drawable[] { - flag = new ClickableUpdateableFlag + flag = new UpdateableFlag { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs index b68ecd709a..27d894cdc2 100644 --- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Rankings.Tables Margin = new MarginPadding { Bottom = row_spacing }, Children = new[] { - new ClickableUpdateableFlag(GetCountryCode(item)) + new UpdateableFlag(GetCountryCode(item)) { Size = new Vector2(28, 20), ShowPlaceholderOnUnknown = false, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 1f922073ec..c79c210e30 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -123,7 +123,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants FillMode = FillMode.Fit, User = user }, - new ClickableUpdateableFlag + new UpdateableFlag { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs index 7234db71b5..85799c03d3 100644 --- a/osu.Game/Screens/Play/HUD/PlayerFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -12,14 +12,14 @@ namespace osu.Game.Screens.Play.HUD { public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable { - private readonly ClickableUpdateableFlag flag; + private readonly UpdateableFlag flag; private const float default_size = 40f; public PlayerFlag() { Size = new Vector2(default_size, default_size / 1.4f); - InternalChild = flag = new ClickableUpdateableFlag + InternalChild = flag = new UpdateableFlag { RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs b/osu.Game/Users/Drawables/UpdateableFlag.cs similarity index 91% rename from osu.Game/Users/Drawables/ClickableUpdateableFlag.cs rename to osu.Game/Users/Drawables/UpdateableFlag.cs index d19234fc17..8f8d7052e5 100644 --- a/osu.Game/Users/Drawables/ClickableUpdateableFlag.cs +++ b/osu.Game/Users/Drawables/UpdateableFlag.cs @@ -11,7 +11,7 @@ using osu.Game.Overlays; namespace osu.Game.Users.Drawables { - public partial class ClickableUpdateableFlag : ModelBackedDrawable + public partial class UpdateableFlag : ModelBackedDrawable { public CountryCode CountryCode { @@ -30,7 +30,7 @@ namespace osu.Game.Users.Drawables /// public Action? Action; - public ClickableUpdateableFlag(CountryCode countryCode = CountryCode.Unknown) + public UpdateableFlag(CountryCode countryCode = CountryCode.Unknown) { CountryCode = countryCode; } diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index e798c8cc11..3c1b68f9ef 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -52,7 +52,7 @@ namespace osu.Game.Users protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar(User, false); - protected ClickableUpdateableFlag CreateFlag() => new ClickableUpdateableFlag(User.CountryCode) + protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.CountryCode) { Size = new Vector2(36, 26), Action = Action, From 82fec4194d9cb676baa2ad8e35a863d21010394a Mon Sep 17 00:00:00 2001 From: yesseruser Date: Wed, 22 Nov 2023 15:45:32 +0100 Subject: [PATCH 831/896] Disabled RecievePositionalInputAtSubTree in PlayerFlag. --- osu.Game/Screens/Play/HUD/PlayerFlag.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs index 85799c03d3..70ad078e34 100644 --- a/osu.Game/Screens/Play/HUD/PlayerFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -12,6 +12,8 @@ namespace osu.Game.Screens.Play.HUD { public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable { + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => false; + private readonly UpdateableFlag flag; private const float default_size = 40f; From 9fd0641238fe8f21ade0528476245d2e23205f56 Mon Sep 17 00:00:00 2001 From: Rowe Wilson Frederisk Holme Date: Thu, 23 Nov 2023 01:18:55 +0800 Subject: [PATCH 832/896] Remove manual changes to Xcode versions in CI --- .github/workflows/ci.yml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11c956bb71..103e4dbc30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,24 +127,14 @@ jobs: build-only-ios: name: Build only (iOS) - # `macos-13` is required, because Xcode 14.3 is required (see below). - # TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta) + # `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3. + # TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images) runs-on: macos-13 timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v3 - # newest Microsoft.iOS.Sdk versions require Xcode 14.3. - # 14.3 is currently not the default Xcode version (https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#xcode), - # so set it manually. - # TODO: remove when 14.3 becomes the default Xcode version. - - name: Set Xcode version - shell: bash - run: | - sudo xcode-select -s "/Applications/Xcode_14.3.app" - echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.3.app" >> $GITHUB_ENV - - name: Install .NET 6.0.x uses: actions/setup-dotnet@v3 with: From 3441a9a5b50bc5093dd06c232a96c4a4cbb5ccc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Nov 2023 08:12:34 +0900 Subject: [PATCH 833/896] Add test coverage for classic scoring overflowing in osu! ruleset --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index cba90b2ebe..c957ddd7d3 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -10,14 +10,17 @@ using NUnit.Framework; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.UI; using osu.Game.Scoring.Legacy; using osu.Game.Tests.Beatmaps; @@ -117,6 +120,35 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.That(scoreProcessor.GetDisplayScore(scoringMode), Is.EqualTo(expectedScore).Within(0.5d)); } + [TestCase(typeof(OsuRuleset))] + [TestCase(typeof(TaikoRuleset))] + [TestCase(typeof(CatchRuleset))] + [TestCase(typeof(ManiaRuleset))] + public void TestBeatmapWithALotOfObjectsDoesNotOverflowClassicScore(Type rulesetType) + { + const int object_count = 999999; + + var ruleset = (Ruleset)Activator.CreateInstance(rulesetType)!; + scoreProcessor = new ScoreProcessor(ruleset); + + var largeBeatmap = new TestBeatmap(ruleset.RulesetInfo) + { + HitObjects = new List(Enumerable.Repeat(new TestHitObject(HitResult.Great), object_count)) + }; + scoreProcessor.ApplyBeatmap(largeBeatmap); + + for (int i = 0; i < object_count; ++i) + { + var judgementResult = new JudgementResult(largeBeatmap.HitObjects[i], largeBeatmap.HitObjects[i].CreateJudgement()) + { + Type = HitResult.Great + }; + scoreProcessor.ApplyResult(judgementResult); + } + + Assert.That(scoreProcessor.GetDisplayScore(ScoringMode.Classic), Is.GreaterThan(0)); + } + [Test] public void TestEmptyBeatmap( [Values(ScoringMode.Standardised, ScoringMode.Classic)] From e28e0ef1cc5847ef4c596ea380c77270089bbcef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Nov 2023 08:15:46 +0900 Subject: [PATCH 834/896] Fix classic scoring overflowing in osu! ruleset due to integer multiplication overflow Closes https://github.com/ppy/osu/issues/25545. --- osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index f6ea5aa455..07c35a334f 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -50,7 +50,7 @@ namespace osu.Game.Scoring.Legacy switch (rulesetId) { case 0: - return (long)Math.Round((objectCount * objectCount * 32.57 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE); + return (long)Math.Round((Math.Pow(objectCount, 2) * 32.57 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE); case 1: return (long)Math.Round((objectCount * 1109 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE); From 4edaaa30839a203eded9bfc16e1dee88b3fb6456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Nov 2023 09:45:38 +0900 Subject: [PATCH 835/896] Add test coverage of skin editor copy-paste --- .../Visual/Gameplay/TestSceneSkinEditor.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs index 9cd87932c8..3c97700fb0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs @@ -335,6 +335,40 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); } + [Test] + public void TestCopyPaste() + { + AddStep("paste", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.V); + InputManager.ReleaseKey(Key.LControl); + }); + // no assertions. just make sure nothing crashes. + + AddStep("select bar hit error blueprint", () => + { + var blueprint = skinEditor.ChildrenOfType().First(b => b.Item is BarHitErrorMeter); + skinEditor.SelectedComponents.Clear(); + skinEditor.SelectedComponents.Add(blueprint.Item); + }); + AddStep("copy", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.C); + InputManager.ReleaseKey(Key.LControl); + }); + AddStep("paste", () => + { + InputManager.PressKey(Key.LControl); + InputManager.Key(Key.V); + InputManager.ReleaseKey(Key.LControl); + }); + AddAssert("three hit error meters present", + () => skinEditor.ChildrenOfType().Count(b => b.Item is BarHitErrorMeter), + () => Is.EqualTo(3)); + } + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler From abbcdaa7f7ad076053daa246668e5020fd8e3462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Nov 2023 09:55:27 +0900 Subject: [PATCH 836/896] Fix skin editor crashing when pasting with nothing in clipboard --- osu.Game/Overlays/SkinEditor/SkinEditor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditor.cs b/osu.Game/Overlays/SkinEditor/SkinEditor.cs index a816031668..f972186333 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditor.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditor.cs @@ -510,6 +510,9 @@ namespace osu.Game.Overlays.SkinEditor protected void Paste() { + if (!canPaste.Value) + return; + changeHandler?.BeginChange(); var drawableInfo = JsonConvert.DeserializeObject(clipboard.Content.Value); From e8d3d26d1607d381718ae0bf3650aa6ad6239c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Nov 2023 09:26:31 +0900 Subject: [PATCH 837/896] Fix slider length not updating when adding new anchor via ctrl-click --- .../Components/PathControlPointVisualiser.cs | 12 ++++----- .../Sliders/SliderSelectionBlueprint.cs | 27 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 1a94d6253d..3add95b2b2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -159,9 +159,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (allowSelection) d.RequestSelection = selectionRequested; - d.DragStarted = dragStarted; - d.DragInProgress = dragInProgress; - d.DragEnded = dragEnded; + d.DragStarted = DragStarted; + d.DragInProgress = DragInProgress; + d.DragEnded = DragEnded; })); Connections.Add(new PathControlPointConnectionPiece(hitObject, e.NewStartingIndex + i)); @@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private int draggedControlPointIndex; private HashSet selectedControlPoints; - private void dragStarted(PathControlPoint controlPoint) + public void DragStarted(PathControlPoint controlPoint) { dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray(); dragPathTypes = hitObject.Path.ControlPoints.Select(point => point.Type).ToArray(); @@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components changeHandler?.BeginChange(); } - private void dragInProgress(DragEvent e) + public void DragInProgress(DragEvent e) { Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray(); var oldPosition = hitObject.Position; @@ -341,7 +341,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components hitObject.Path.ControlPoints[i].Type = dragPathTypes[i]; } - private void dragEnded() => changeHandler?.EndChange(); + public void DragEnded() => changeHandler?.EndChange(); #endregion diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 80c4cee7f2..a4b8064f05 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -39,9 +39,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [CanBeNull] protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } - [Resolved(CanBeNull = true)] - private IPositionSnapProvider positionSnapProvider { get; set; } - [Resolved(CanBeNull = true)] private IDistanceSnapProvider distanceSnapProvider { get; set; } @@ -191,15 +188,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [CanBeNull] private PathControlPoint placementControlPoint; - protected override bool OnDragStart(DragStartEvent e) => placementControlPoint != null; + protected override bool OnDragStart(DragStartEvent e) + { + if (placementControlPoint == null) + return base.OnDragStart(e); + + ControlPointVisualiser?.DragStarted(placementControlPoint); + return true; + } protected override void OnDrag(DragEvent e) { + base.OnDrag(e); + if (placementControlPoint != null) - { - var result = positionSnapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition)); - placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position; - } + ControlPointVisualiser?.DragInProgress(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + + if (placementControlPoint != null) + ControlPointVisualiser?.DragEnded(); } protected override void OnMouseUp(MouseUpEvent e) From 74966cae6ad9a98454731f19123a7044ff2d7e0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Nov 2023 10:31:58 +0900 Subject: [PATCH 838/896] Remove unused using directive --- osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs index f810bbf155..16f28c0212 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuHealthProcessor.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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; From 7998204cfe1e529bc684b54f64356882ec2c81a2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Nov 2023 13:41:01 +0900 Subject: [PATCH 839/896] Fix combo/combo colouring issues around spinners --- .../Beatmaps/CatchBeatmapProcessor.cs | 16 ++++++ .../Objects/CatchHitObject.cs | 25 +++++++++ .../Beatmaps/OsuBeatmapProcessor.cs | 54 ++++++++++++------- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 25 +++++++++ .../Formats/LegacyBeatmapDecoderTest.cs | 48 +++++++++++++++++ .../Resources/spinner-between-objects.osu | 38 +++++++++++++ osu.Game/Beatmaps/BeatmapProcessor.cs | 6 --- .../Legacy/Catch/ConvertHitObjectParser.cs | 38 ++++--------- .../Legacy/Osu/ConvertHitObjectParser.cs | 38 ++++--------- 9 files changed, 210 insertions(+), 78 deletions(-) create mode 100644 osu.Game.Tests/Resources/spinner-between-objects.osu diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index ab61b14ac4..7c81ca03d1 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -23,6 +23,22 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { } + public override void PreProcess() + { + IHasComboInformation? lastObj = null; + + // For sanity, ensures that both the first hitobject and the first hitobject after a banana shower start a new combo. + // This is normally enforced by the legacy decoder, but is not enforced by the editor. + foreach (var obj in Beatmap.HitObjects.OfType()) + { + if (obj is not BananaShower && (lastObj == null || lastObj is BananaShower)) + obj.NewCombo = true; + lastObj = obj; + } + + base.PreProcess(); + } + public override void PostProcess() { base.PostProcess(); diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index b9fef6bf8c..d122758a4e 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -155,6 +155,31 @@ namespace osu.Game.Rulesets.Catch.Objects Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize); } + public void UpdateComboInformation(IHasComboInformation? lastObj) + { + ComboIndex = lastObj?.ComboIndex ?? 0; + ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + + if (this is BananaShower) + { + // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + return; + } + + // At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (NewCombo || lastObj == null || lastObj is BananaShower) + { + IndexInCurrentCombo = 0; + ComboIndex++; + ComboIndexWithOffsets += ComboOffset + 1; + + if (lastObj != null) + lastObj.LastInCombo = true; + } + } + protected override HitWindows CreateHitWindows() => HitWindows.Empty; #region Hit object conversion diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index c081df3ac6..835c67ff19 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -19,6 +21,22 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { } + public override void PreProcess() + { + IHasComboInformation? lastObj = null; + + // For sanity, ensures that both the first hitobject and the first hitobject after a spinner start a new combo. + // This is normally enforced by the legacy decoder, but is not enforced by the editor. + foreach (var obj in Beatmap.HitObjects.OfType()) + { + if (obj is not Spinner && (lastObj == null || lastObj is Spinner)) + obj.NewCombo = true; + lastObj = obj; + } + + base.PreProcess(); + } + public override void PostProcess() { base.PostProcess(); @@ -95,15 +113,15 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { int n = i; /* We should check every note which has not yet got a stack. - * Consider the case we have two interwound stacks and this will make sense. - * - * o <-1 o <-2 - * o <-3 o <-4 - * - * We first process starting from 4 and handle 2, - * then we come backwards on the i loop iteration until we reach 3 and handle 1. - * 2 and 1 will be ignored in the i loop because they already have a stack value. - */ + * Consider the case we have two interwound stacks and this will make sense. + * + * o <-1 o <-2 + * o <-3 o <-4 + * + * We first process starting from 4 and handle 2, + * then we come backwards on the i loop iteration until we reach 3 and handle 1. + * 2 and 1 will be ignored in the i loop because they already have a stack value. + */ OsuHitObject objectI = beatmap.HitObjects[i]; if (objectI.StackHeight != 0 || objectI is Spinner) continue; @@ -111,9 +129,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps double stackThreshold = objectI.TimePreempt * beatmap.BeatmapInfo.StackLeniency; /* If this object is a hitcircle, then we enter this "special" case. - * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. - * Any other case is handled by the "is Slider" code below this. - */ + * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. + * Any other case is handled by the "is Slider" code below this. + */ if (objectI is HitCircle) { while (--n >= 0) @@ -135,10 +153,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps } /* This is a special case where hticircles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern. - * o==o <- slider is at original location - * o <- hitCircle has stack of -1 - * o <- hitCircle has stack of -2 - */ + * o==o <- slider is at original location + * o <- hitCircle has stack of -1 + * o <- hitCircle has stack of -2 + */ if (objectN is Slider && Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < stack_distance) { int offset = objectI.StackHeight - objectN.StackHeight + 1; @@ -169,8 +187,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps else if (objectI is Slider) { /* We have hit the first slider in a possible stack. - * From this point on, we ALWAYS stack positive regardless. - */ + * From this point on, we ALWAYS stack positive regardless. + */ while (--n >= startIndex) { OsuHitObject objectN = beatmap.HitObjects[n]; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index d74d28c748..716c34d024 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -159,6 +159,31 @@ namespace osu.Game.Rulesets.Osu.Objects Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize, true); } + public void UpdateComboInformation(IHasComboInformation? lastObj) + { + ComboIndex = lastObj?.ComboIndex ?? 0; + ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; + IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; + + if (this is Spinner) + { + // For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so. + return; + } + + // At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo, + // but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here. + if (NewCombo || lastObj == null || lastObj is Spinner) + { + IndexInCurrentCombo = 0; + ComboIndex++; + ComboIndexWithOffsets += ComboOffset + 1; + + if (lastObj != null) + lastObj.LastInCombo = true; + } + } + protected override HitWindows CreateHitWindows() => new OsuHitWindows(); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index be1993957f..20f59384ab 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.IO; +using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Mods; @@ -1108,5 +1109,52 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(((IHasCombo)beatmap.HitObjects[2]).NewCombo, Is.False); } } + + /// + /// Test cases that involve a spinner between two hitobjects. + /// + [Test] + public void TestSpinnerNewComboBetweenObjects([Values("osu", "catch")] string rulesetName) + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("spinner-between-objects.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + Ruleset ruleset; + + switch (rulesetName) + { + case "osu": + ruleset = new OsuRuleset(); + break; + + case "catch": + ruleset = new CatchRuleset(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(rulesetName), rulesetName, null); + } + + var working = new TestWorkingBeatmap(decoder.Decode(stream)); + var playable = working.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty()); + + // There's no good way to figure out these values other than to compare (in code) with osu!stable... + + Assert.That(((IHasComboInformation)playable.HitObjects[0]).ComboIndexWithOffsets, Is.EqualTo(1)); + Assert.That(((IHasComboInformation)playable.HitObjects[2]).ComboIndexWithOffsets, Is.EqualTo(2)); + Assert.That(((IHasComboInformation)playable.HitObjects[3]).ComboIndexWithOffsets, Is.EqualTo(2)); + Assert.That(((IHasComboInformation)playable.HitObjects[5]).ComboIndexWithOffsets, Is.EqualTo(3)); + Assert.That(((IHasComboInformation)playable.HitObjects[6]).ComboIndexWithOffsets, Is.EqualTo(3)); + Assert.That(((IHasComboInformation)playable.HitObjects[8]).ComboIndexWithOffsets, Is.EqualTo(4)); + Assert.That(((IHasComboInformation)playable.HitObjects[9]).ComboIndexWithOffsets, Is.EqualTo(4)); + Assert.That(((IHasComboInformation)playable.HitObjects[11]).ComboIndexWithOffsets, Is.EqualTo(5)); + Assert.That(((IHasComboInformation)playable.HitObjects[12]).ComboIndexWithOffsets, Is.EqualTo(6)); + Assert.That(((IHasComboInformation)playable.HitObjects[14]).ComboIndexWithOffsets, Is.EqualTo(7)); + Assert.That(((IHasComboInformation)playable.HitObjects[15]).ComboIndexWithOffsets, Is.EqualTo(8)); + Assert.That(((IHasComboInformation)playable.HitObjects[17]).ComboIndexWithOffsets, Is.EqualTo(9)); + } + } } } diff --git a/osu.Game.Tests/Resources/spinner-between-objects.osu b/osu.Game.Tests/Resources/spinner-between-objects.osu new file mode 100644 index 0000000000..03e61d965c --- /dev/null +++ b/osu.Game.Tests/Resources/spinner-between-objects.osu @@ -0,0 +1,38 @@ +osu file format v14 + +[General] +Mode: 0 + +[TimingPoints] +0,571.428571428571,4,2,1,5,1,0 + +[HitObjects] +// +C -> +C -> +C +104,95,0,5,0,0:0:0:0: +256,192,1000,12,0,2000,0:0:0:0: +178,171,3000,5,0,0:0:0:0: + +// -C -> +C -> +C +178,171,4000,1,0,0:0:0:0: +256,192,5000,12,0,6000,0:0:0:0: +178,171,7000,5,0,0:0:0:0: + +// -C -> -C -> +C +178,171,8000,1,0,0:0:0:0: +256,192,9000,8,0,10000,0:0:0:0: +178,171,11000,5,0,0:0:0:0: + +// -C -> -C -> -C +178,171,12000,1,0,0:0:0:0: +256,192,13000,8,0,14000,0:0:0:0: +178,171,15000,1,0,0:0:0:0: + +// +C -> -C -> -C +178,171,16000,5,0,0:0:0:0: +256,192,17000,8,0,18000,0:0:0:0: +178,171,19000,1,0,0:0:0:0: + +// +C -> +C -> -C +178,171,20000,5,0,0:0:0:0: +256,192,21000,12,0,22000,0:0:0:0: +178,171,23000,1,0,0:0:0:0: \ No newline at end of file diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index fb5313469f..89d6e9d3f8 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -24,12 +24,6 @@ namespace osu.Game.Beatmaps foreach (var obj in Beatmap.HitObjects.OfType()) { - if (lastObj == null) - { - // first hitobject should always be marked as a new combo for sanity. - obj.NewCombo = true; - } - obj.UpdateComboInformation(lastObj); lastObj = obj; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index 4861e8b3f7..0ed5aef0cf 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -14,26 +14,19 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { + private ConvertHitObject lastObject; + public ConvertHitObjectParser(double offset, int formatVersion) : base(offset, formatVersion) { } - private bool forceNewCombo; - private int extraComboOffset; - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) { - newCombo |= forceNewCombo; - comboOffset += extraComboOffset; - - forceNewCombo = false; - extraComboOffset = 0; - - return new ConvertHit + return lastObject = new ConvertHit { Position = position, - NewCombo = newCombo, + NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, ComboOffset = comboOffset }; } @@ -41,16 +34,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, IList> nodeSamples) { - newCombo |= forceNewCombo; - comboOffset += extraComboOffset; - - forceNewCombo = false; - extraComboOffset = 0; - - return new ConvertSlider + return lastObject = new ConvertSlider { Position = position, - NewCombo = FirstObject || newCombo, + NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, ComboOffset = comboOffset, Path = new SliderPath(controlPoints, length), NodeSamples = nodeSamples, @@ -60,20 +47,17 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { - // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo - // Their combo offset is still added to that next hitobject's combo index - forceNewCombo |= FormatVersion <= 8 || newCombo; - extraComboOffset += comboOffset; - - return new ConvertSpinner + return lastObject = new ConvertSpinner { - Duration = duration + Duration = duration, + NewCombo = newCombo + // Spinners cannot have combo offset. }; } protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { - return null; + return lastObject = null; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 7a88a31bd5..8bb9600a7d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -14,26 +14,19 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu ///
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { + private ConvertHitObject lastObject; + public ConvertHitObjectParser(double offset, int formatVersion) : base(offset, formatVersion) { } - private bool forceNewCombo; - private int extraComboOffset; - protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) { - newCombo |= forceNewCombo; - comboOffset += extraComboOffset; - - forceNewCombo = false; - extraComboOffset = 0; - - return new ConvertHit + return lastObject = new ConvertHit { Position = position, - NewCombo = FirstObject || newCombo, + NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, ComboOffset = comboOffset }; } @@ -41,16 +34,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount, IList> nodeSamples) { - newCombo |= forceNewCombo; - comboOffset += extraComboOffset; - - forceNewCombo = false; - extraComboOffset = 0; - - return new ConvertSlider + return lastObject = new ConvertSlider { Position = position, - NewCombo = FirstObject || newCombo, + NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, ComboOffset = comboOffset, Path = new SliderPath(controlPoints, length), NodeSamples = nodeSamples, @@ -60,21 +47,18 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration) { - // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo - // Their combo offset is still added to that next hitobject's combo index - forceNewCombo |= FormatVersion <= 8 || newCombo; - extraComboOffset += comboOffset; - - return new ConvertSpinner + return lastObject = new ConvertSpinner { Position = position, - Duration = duration + Duration = duration, + NewCombo = newCombo + // Spinners cannot have combo offset. }; } protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration) { - return null; + return lastObject = null; } } } From 3da8a0cbed624319841b479e2cced91fa8608cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 23 Nov 2023 14:00:42 +0900 Subject: [PATCH 840/896] Fix undo being broken when ctrl-click and dragging new point --- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index a4b8064f05..b3efe1c495 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -205,18 +205,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders ControlPointVisualiser?.DragInProgress(e); } - protected override void OnDragEnd(DragEndEvent e) - { - base.OnDragEnd(e); - - if (placementControlPoint != null) - ControlPointVisualiser?.DragEnded(); - } - protected override void OnMouseUp(MouseUpEvent e) { if (placementControlPoint != null) { + if (IsDragged) + ControlPointVisualiser?.DragEnded(); + placementControlPoint = null; changeHandler?.EndChange(); } From 191e8c5487ccd4ea1b85aa7f47e6454f5a180c1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 16:39:05 +0900 Subject: [PATCH 841/896] Add note about skin editor reload jank --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index cbe122395c..d1e7b97efc 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -210,6 +210,9 @@ namespace osu.Game.Overlays.SkinEditor // The skin editor doesn't work well if beatmap skins are being applied to the player screen. // To keep things simple, disable the setting game-wide while using the skin editor. + // + // This causes a full reload of the skin, which is pretty ugly. + // TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap. leasedBeatmapSkins = beatmapSkins.BeginLease(true); leasedBeatmapSkins.Value = false; } From a80a5be4ec01c179069b57ee384a7196670a0f85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 17:11:40 +0900 Subject: [PATCH 842/896] Fix a couple of new r# inspections --- osu.Game/Beatmaps/Beatmap.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 4f81b26c3e..0cafd4e08a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -119,12 +119,11 @@ namespace osu.Game.Beatmaps IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() => (Beatmap)MemberwiseClone(); + + public override string ToString() => BeatmapInfo.ToString(); } public class Beatmap : Beatmap { - public new Beatmap Clone() => (Beatmap)base.Clone(); - - public override string ToString() => BeatmapInfo?.ToString() ?? base.ToString(); } } From 5239fee947435bf4fe99b43d28a7fe0118ca64b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 17:15:03 +0900 Subject: [PATCH 843/896] Allow use of skin username/flag/avatar components outside of gameplay --- osu.Game/Screens/Play/HUD/PlayerAvatar.cs | 20 ++++++++++++++++++-- osu.Game/Screens/Play/HUD/PlayerFlag.cs | 22 ++++++++++++++++++++-- osu.Game/Skinning/Components/PlayerName.cs | 21 +++++++++++++++++++-- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs index 1341a10d60..06d0f7bc9a 100644 --- a/osu.Game/Screens/Play/HUD/PlayerAvatar.cs +++ b/osu.Game/Screens/Play/HUD/PlayerAvatar.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Configuration; using osu.Game.Localisation.SkinComponents; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Settings; using osu.Game.Skinning; using osu.Game.Users.Drawables; @@ -29,6 +31,14 @@ namespace osu.Game.Screens.Play.HUD private const float default_size = 80f; + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + public PlayerAvatar() { Size = new Vector2(default_size); @@ -41,9 +51,15 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(GameplayState gameplayState) + private void load() { - avatar.User = gameplayState.Score.ScoreInfo.User; + if (gameplayState != null) + avatar.User = gameplayState.Score.ScoreInfo.User; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => avatar.User = u.NewValue, true); + } } protected override void LoadComplete() diff --git a/osu.Game/Screens/Play/HUD/PlayerFlag.cs b/osu.Game/Screens/Play/HUD/PlayerFlag.cs index 70ad078e34..c7e247d26a 100644 --- a/osu.Game/Screens/Play/HUD/PlayerFlag.cs +++ b/osu.Game/Screens/Play/HUD/PlayerFlag.cs @@ -2,8 +2,11 @@ // 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.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Skinning; using osu.Game.Users.Drawables; using osuTK; @@ -18,9 +21,18 @@ namespace osu.Game.Screens.Play.HUD private const float default_size = 40f; + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + public PlayerFlag() { Size = new Vector2(default_size, default_size / 1.4f); + InternalChild = flag = new UpdateableFlag { RelativeSizeAxes = Axes.Both, @@ -28,9 +40,15 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(GameplayState gameplayState) + private void load() { - flag.CountryCode = gameplayState.Score.ScoreInfo.User.CountryCode; + if (gameplayState != null) + flag.CountryCode = gameplayState.Score.ScoreInfo.User.CountryCode; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => flag.CountryCode = u.NewValue.CountryCode, true); + } } public bool UsesFixedAnchor { get; set; } diff --git a/osu.Game/Skinning/Components/PlayerName.cs b/osu.Game/Skinning/Components/PlayerName.cs index 34ace53d47..21bf615bc6 100644 --- a/osu.Game/Skinning/Components/PlayerName.cs +++ b/osu.Game/Skinning/Components/PlayerName.cs @@ -3,9 +3,12 @@ using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Play; namespace osu.Game.Skinning.Components @@ -15,6 +18,14 @@ namespace osu.Game.Skinning.Components { private readonly OsuSpriteText text; + [Resolved] + private GameplayState? gameplayState { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable? apiUser; + public PlayerName() { AutoSizeAxes = Axes.Both; @@ -30,9 +41,15 @@ namespace osu.Game.Skinning.Components } [BackgroundDependencyLoader] - private void load(GameplayState gameplayState) + private void load() { - text.Text = gameplayState.Score.ScoreInfo.User.Username; + if (gameplayState != null) + text.Text = gameplayState.Score.ScoreInfo.User.Username; + else + { + apiUser = api.LocalUser.GetBoundCopy(); + apiUser.BindValueChanged(u => text.Text = u.NewValue.Username, true); + } } protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40); From 268b965ee890a3ea10ea152465eae6faede94e6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 17:28:16 +0900 Subject: [PATCH 844/896] Enable NRT on `Beatmap` --- osu.Game/Beatmaps/Beatmap.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 0cafd4e08a..6db9febf36 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.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; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; @@ -26,8 +24,7 @@ namespace osu.Game.Beatmaps { difficulty = value; - if (beatmapInfo != null) - beatmapInfo.Difficulty = difficulty.Clone(); + beatmapInfo.Difficulty = difficulty.Clone(); } } @@ -40,8 +37,7 @@ namespace osu.Game.Beatmaps { beatmapInfo = value; - if (beatmapInfo?.Difficulty != null) - Difficulty = beatmapInfo.Difficulty.Clone(); + Difficulty = beatmapInfo.Difficulty.Clone(); } } From 10e16e4b04c58774573c1b8fff66ee46b717bcdc Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 09:46:06 +0900 Subject: [PATCH 845/896] Fix handling of combo offset without new combo, and incorrect lazer tests --- .../Formats/LegacyBeatmapDecoderTest.cs | 24 +++++++++---------- .../Resources/hitobject-combo-offset.osu | 12 +++++----- .../Legacy/Catch/ConvertHitObjectParser.cs | 4 ++-- .../Legacy/Osu/ConvertHitObjectParser.cs | 4 ++-- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index cccceaf58e..dcfe8ecb41 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -434,12 +434,12 @@ namespace osu.Game.Tests.Beatmaps.Formats new OsuBeatmapProcessor(converted).PreProcess(); new OsuBeatmapProcessor(converted).PostProcess(); - Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets); - Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets); - Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets); - Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets); - Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets); - Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets); + Assert.AreEqual(1, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets); + Assert.AreEqual(2, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets); + Assert.AreEqual(3, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets); + Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets); + Assert.AreEqual(8, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets); + Assert.AreEqual(9, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets); } } @@ -457,12 +457,12 @@ namespace osu.Game.Tests.Beatmaps.Formats new CatchBeatmapProcessor(converted).PreProcess(); new CatchBeatmapProcessor(converted).PostProcess(); - Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets); - Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets); - Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets); - Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets); - Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets); - Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets); + Assert.AreEqual(1, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndexWithOffsets); + Assert.AreEqual(2, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndexWithOffsets); + Assert.AreEqual(3, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndexWithOffsets); + Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndexWithOffsets); + Assert.AreEqual(8, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndexWithOffsets); + Assert.AreEqual(9, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndexWithOffsets); } } diff --git a/osu.Game.Tests/Resources/hitobject-combo-offset.osu b/osu.Game.Tests/Resources/hitobject-combo-offset.osu index d39a3e8548..9f39229d87 100644 --- a/osu.Game.Tests/Resources/hitobject-combo-offset.osu +++ b/osu.Game.Tests/Resources/hitobject-combo-offset.osu @@ -3,30 +3,30 @@ osu file format v14 [HitObjects] // Circle with combo offset (3) 255,193,1000,49,0,0:0:0:0: -// Combo index = 4 +// Combo index = 1 // Spinner with new combo followed by circle with no new combo 256,192,2000,12,0,2000,0:0:0:0: 255,193,3000,1,0,0:0:0:0: -// Combo index = 5 +// Combo index = 2 // Spinner without new combo followed by circle with no new combo 256,192,4000,8,0,5000,0:0:0:0: 255,193,6000,1,0,0:0:0:0: -// Combo index = 5 +// Combo index = 3 // Spinner without new combo followed by circle with new combo 256,192,7000,8,0,8000,0:0:0:0: 255,193,9000,5,0,0:0:0:0: -// Combo index = 6 +// Combo index = 4 // Spinner with new combo and offset (1) followed by circle with new combo and offset (3) 256,192,10000,28,0,11000,0:0:0:0: 255,193,12000,53,0,0:0:0:0: -// Combo index = 11 +// Combo index = 8 // Spinner with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo 256,192,13000,44,0,14000,0:0:0:0: 256,192,15000,8,0,16000,0:0:0:0: 255,193,17000,1,0,0:0:0:0: -// Combo index = 14 \ No newline at end of file +// Combo index = 9 \ No newline at end of file diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index 0ed5aef0cf..a5c1a73fa7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch { Position = position, NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = comboOffset + ComboOffset = newCombo ? comboOffset : 0 }; } @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch { Position = position, NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = comboOffset, + ComboOffset = newCombo ? comboOffset : 0, Path = new SliderPath(controlPoints, length), NodeSamples = nodeSamples, RepeatCount = repeatCount diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 8bb9600a7d..43c346b621 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu { Position = position, NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = comboOffset + ComboOffset = newCombo ? comboOffset : 0 }; } @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu { Position = position, NewCombo = FirstObject || lastObject is ConvertSpinner || newCombo, - ComboOffset = comboOffset, + ComboOffset = newCombo ? comboOffset : 0, Path = new SliderPath(controlPoints, length), NodeSamples = nodeSamples, RepeatCount = repeatCount From 039f8e62421d13d23f532e146cadd97ec89e78d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 10:25:23 +0900 Subject: [PATCH 846/896] Add note about shared code --- osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs | 2 ++ osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index d122758a4e..17ff8afb87 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -157,6 +157,8 @@ namespace osu.Game.Rulesets.Catch.Objects public void UpdateComboInformation(IHasComboInformation? lastObj) { + // Note that this implementation is shared with the osu! ruleset's implementation. + // If a change is made here, OsuHitObject.cs should also be updated. ComboIndex = lastObj?.ComboIndex ?? 0; ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 716c34d024..21f7b4b22d 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -161,6 +161,8 @@ namespace osu.Game.Rulesets.Osu.Objects public void UpdateComboInformation(IHasComboInformation? lastObj) { + // Note that this implementation is shared with the osu!catch ruleset's implementation. + // If a change is made here, CatchHitObject.cs should also be updated. ComboIndex = lastObj?.ComboIndex ?? 0; ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0; IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0; From 61d5a890f724a9bca05fd8e0842e47eeae7626a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 24 Nov 2023 12:37:02 +0900 Subject: [PATCH 847/896] Update links to figma library --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9f7d88f5c7..4106641adb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive. -If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library). +If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library). Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes: @@ -85,4 +85,4 @@ If you're uncertain about some part of the codebase or some inner workings of th - [Development roadmap](https://github.com/orgs/ppy/projects/7/views/6): What the core team is currently working on - [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game - [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game -- [Public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library): Contains finished and draft designs for osu! +- [Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library): Contains finished and draft designs for osu! From 537b0ae870d3fb4e1023c8418ce7badb71cb8cb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 12:47:40 +0900 Subject: [PATCH 848/896] Add silly annotation for now (more new r# rules) --- osu.Game/Screens/Ranking/ScorePanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 1d332d6b27..1f7ba3692a 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -82,6 +83,7 @@ namespace osu.Game.Screens.Ranking private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); + [CanBeNull] public event Action StateChanged; /// From 95c00f966627c00648ed4c72848962676aad8eff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 12:45:55 +0900 Subject: [PATCH 849/896] Add `HexaconIcons` lookup to allow usage with `SpriteIcon` --- osu.Game/Graphics/HexaconsIcons.cs | 112 ++++++++++++++++++ osu.Game/OsuGameBase.cs | 2 + .../Play/HUD/ArgonCounterTextComponent.cs | 2 +- osu.Game/Skinning/LegacySpriteText.cs | 2 +- 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Graphics/HexaconsIcons.cs diff --git a/osu.Game/Graphics/HexaconsIcons.cs b/osu.Game/Graphics/HexaconsIcons.cs new file mode 100644 index 0000000000..9b0fb30963 --- /dev/null +++ b/osu.Game/Graphics/HexaconsIcons.cs @@ -0,0 +1,112 @@ +// 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.Threading; +using System.Threading.Tasks; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Text; + +namespace osu.Game.Graphics +{ + public static class HexaconsIcons + { + public const string FONT_NAME = "Icons/Hexacons"; + + public static IconUsage Editor => get(HexaconsMapping.editor); + + private static IconUsage get(HexaconsMapping icon) + { + return new IconUsage((char)icon, FONT_NAME); + } + + // Basically just converting to something we can use in a `char` lookup for FontStore/GlyphStore compatibility. + // Names should match filenames in resources. + private enum HexaconsMapping + { + beatmap_packs, + beatmap, + calendar, + chart, + community, + contests, + devtools, + download, + editor, + featured_artist, + home, + messaging, + music, + news, + notification, + profile, + rankings, + search, + settings, + social, + store, + tournament, + wiki, + } + + public class HexaconsStore : ITextureStore, ITexturedGlyphLookupStore + { + private readonly TextureStore textures; + + public HexaconsStore(TextureStore textures) + { + this.textures = textures; + } + + public void Dispose() + { + textures.Dispose(); + } + + public ITexturedCharacterGlyph? Get(string? fontName, char character) + { + if (fontName == FONT_NAME) + return new Glyph(textures.Get($"{fontName}/{((HexaconsMapping)character).ToString().Replace("_", "-")}")); + + return null; + } + + public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); + + public Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; + + public Texture Get(string name) => throw new NotImplementedException(); + + public Task GetAsync(string name, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public Stream GetStream(string name) => throw new NotImplementedException(); + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public Task GetAsync(string name, WrapMode wrapModeS, WrapMode wrapModeT, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + public class Glyph : ITexturedCharacterGlyph + { + public float XOffset => default; + public float YOffset => default; + public float XAdvance => default; + public float Baseline => default; + public char Character => default; + + public float GetKerning(T lastGlyph) where T : ICharacterGlyph => throw new NotImplementedException(); + + public Texture Texture { get; } + public float Width => Texture.Width; + public float Height => Texture.Height; + + public Glyph(Texture texture) + { + Texture = texture; + } + } + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 228edc8952..2d8024a45a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -477,6 +477,8 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera/Venera-Light"); AddFont(Resources, @"Fonts/Venera/Venera-Bold"); AddFont(Resources, @"Fonts/Venera/Venera-Black"); + + Fonts.AddStore(new HexaconsIcons.HexaconsStore(Textures)); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index d3fadb452b..2a3f4365cb 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Play.HUD this.getLookup = getLookup; } - public ITexturedCharacterGlyph? Get(string fontName, char character) + public ITexturedCharacterGlyph? Get(string? fontName, char character) { string lookup = getLookup(character); var texture = textures.Get($"Gameplay/Fonts/{fontName}-{lookup}"); diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 041a32e8de..8aefa50252 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -63,7 +63,7 @@ namespace osu.Game.Skinning this.maxSize = maxSize; } - public ITexturedCharacterGlyph? Get(string fontName, char character) + public ITexturedCharacterGlyph? Get(string? fontName, char character) { string lookup = getLookupName(character); From 340227a06d65c6b8bb1edf2e4834ab04d7f809ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 13:16:04 +0900 Subject: [PATCH 850/896] Replace all hexacon lookups with strongly typed properties --- .../UserInterface/TestSceneOverlayHeader.cs | 3 +- osu.Game/Graphics/HexaconsIcons.cs | 27 +++++++++-- .../BeatmapListing/BeatmapListingHeader.cs | 3 +- .../Overlays/BeatmapSet/BeatmapSetHeader.cs | 3 +- .../Overlays/Changelog/ChangelogHeader.cs | 3 +- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 4 +- osu.Game/Overlays/ChatOverlay.cs | 4 +- .../Dashboard/DashboardOverlayHeader.cs | 3 +- osu.Game/Overlays/FullscreenOverlay.cs | 3 +- osu.Game/Overlays/INamedOverlayComponent.cs | 3 +- osu.Game/Overlays/News/NewsHeader.cs | 3 +- osu.Game/Overlays/NotificationOverlay.cs | 4 +- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- osu.Game/Overlays/OverlayTitle.cs | 45 ++++++------------- osu.Game/Overlays/Profile/ProfileHeader.cs | 3 +- .../Rankings/RankingsOverlayHeader.cs | 3 +- osu.Game/Overlays/SettingsOverlay.cs | 4 +- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 12 ++--- .../Overlays/Toolbar/ToolbarHomeButton.cs | 3 +- .../Toolbar/ToolbarOverlayToggleButton.cs | 2 +- osu.Game/Overlays/Wiki/WikiHeader.cs | 3 +- .../Edit/Components/Menus/EditorMenuBar.cs | 4 +- .../Screens/Edit/Setup/SetupScreenHeader.cs | 3 +- 23 files changed, 82 insertions(+), 65 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index a927b0931b..55a04b129c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; using osuTK.Graphics; namespace osu.Game.Tests.Visual.UserInterface @@ -153,7 +154,7 @@ namespace osu.Game.Tests.Visual.UserInterface public TestTitle() { Title = "title"; - IconTexture = "Icons/changelog"; + Icon = HexaconsIcons.Devtools; } } } diff --git a/osu.Game/Graphics/HexaconsIcons.cs b/osu.Game/Graphics/HexaconsIcons.cs index 9b0fb30963..3eee5d7197 100644 --- a/osu.Game/Graphics/HexaconsIcons.cs +++ b/osu.Game/Graphics/HexaconsIcons.cs @@ -16,12 +16,31 @@ namespace osu.Game.Graphics { public const string FONT_NAME = "Icons/Hexacons"; + public static IconUsage BeatmapPacks => get(HexaconsMapping.beatmap_packs); + public static IconUsage Beatmap => get(HexaconsMapping.beatmap); + public static IconUsage Calendar => get(HexaconsMapping.calendar); + public static IconUsage Chart => get(HexaconsMapping.chart); + public static IconUsage Community => get(HexaconsMapping.community); + public static IconUsage Contests => get(HexaconsMapping.contests); + public static IconUsage Devtools => get(HexaconsMapping.devtools); + public static IconUsage Download => get(HexaconsMapping.download); public static IconUsage Editor => get(HexaconsMapping.editor); + public static IconUsage FeaturedArtist => get(HexaconsMapping.featured_artist); + public static IconUsage Home => get(HexaconsMapping.home); + public static IconUsage Messaging => get(HexaconsMapping.messaging); + public static IconUsage Music => get(HexaconsMapping.music); + public static IconUsage News => get(HexaconsMapping.news); + public static IconUsage Notification => get(HexaconsMapping.notification); + public static IconUsage Profile => get(HexaconsMapping.profile); + public static IconUsage Rankings => get(HexaconsMapping.rankings); + public static IconUsage Search => get(HexaconsMapping.search); + public static IconUsage Settings => get(HexaconsMapping.settings); + public static IconUsage Social => get(HexaconsMapping.social); + public static IconUsage Store => get(HexaconsMapping.store); + public static IconUsage Tournament => get(HexaconsMapping.tournament); + public static IconUsage Wiki => get(HexaconsMapping.wiki); - private static IconUsage get(HexaconsMapping icon) - { - return new IconUsage((char)icon, FONT_NAME); - } + private static IconUsage get(HexaconsMapping icon) => new IconUsage((char)icon, FONT_NAME); // Basically just converting to something we can use in a `char` lookup for FontStore/GlyphStore compatibility. // Names should match filenames in resources. diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 3336c383ff..27fab82bf3 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -23,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapListing { Title = PageTitleStrings.MainBeatmapsetsControllerIndex; Description = NamedOverlayComponentStrings.BeatmapListingDescription; - IconTexture = "Icons/Hexacons/beatmap"; + Icon = HexaconsIcons.Beatmap; } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index 858742648c..eced27f35e 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; @@ -59,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet public BeatmapHeaderTitle() { Title = PageTitleStrings.MainBeatmapsetsControllerShow; - IconTexture = "Icons/Hexacons/beatmap"; + Icon = HexaconsIcons.Beatmap; } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index e9be67e977..61ea9dc4db 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -123,7 +124,7 @@ namespace osu.Game.Overlays.Changelog { Title = PageTitleStrings.MainChangelogControllerDefault; Description = NamedOverlayComponentStrings.ChangelogDescription; - IconTexture = "Icons/Hexacons/devtools"; + Icon = HexaconsIcons.Devtools; } } } diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 0410174dc1..c6f63a72b9 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -45,11 +45,11 @@ namespace osu.Game.Overlays.Chat { new Drawable[] { - new Sprite + new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = textures.Get("Icons/Hexacons/messaging"), + Icon = HexaconsIcons.Social, Size = new Vector2(18), }, // Placeholder text diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 724f77ad71..5cf2ac6c86 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -11,11 +11,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; @@ -29,7 +31,7 @@ namespace osu.Game.Overlays { public partial class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler { - public string IconTexture => "Icons/Hexacons/messaging"; + public IconUsage Icon => HexaconsIcons.Messaging; public LocalisableString Title => ChatStrings.HeaderTitle; public LocalisableString Description => ChatStrings.HeaderDescription; diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 0f4697e33c..b9d869c2ec 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Dashboard { Title = PageTitleStrings.MainHomeControllerIndex; Description = NamedOverlayComponentStrings.DashboardDescription; - IconTexture = "Icons/Hexacons/social"; + Icon = HexaconsIcons.Social; } } } diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 6ee045c492..6ddf1eecf0 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -17,7 +18,7 @@ namespace osu.Game.Overlays public abstract partial class FullscreenOverlay : WaveOverlayContainer, INamedOverlayComponent where T : OverlayHeader { - public virtual string IconTexture => Header.Title.IconTexture; + public virtual IconUsage Icon => Header.Title.Icon; public virtual LocalisableString Title => Header.Title.Title; public virtual LocalisableString Description => Header.Title.Description; diff --git a/osu.Game/Overlays/INamedOverlayComponent.cs b/osu.Game/Overlays/INamedOverlayComponent.cs index 65664b12e7..ef3c029aac 100644 --- a/osu.Game/Overlays/INamedOverlayComponent.cs +++ b/osu.Game/Overlays/INamedOverlayComponent.cs @@ -1,13 +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.Graphics.Sprites; using osu.Framework.Localisation; namespace osu.Game.Overlays { public interface INamedOverlayComponent { - string IconTexture { get; } + IconUsage Icon { get; } LocalisableString Title { get; } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index 44e2f6a8cb..f237ed66f2 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -7,6 +7,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; @@ -68,7 +69,7 @@ namespace osu.Game.Overlays.News { Title = PageTitleStrings.MainNewsControllerDefault; Description = NamedOverlayComponentStrings.NewsDescription; - IconTexture = "Icons/Hexacons/news"; + Icon = HexaconsIcons.News; } } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 81233b4343..c3ddb228ea 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -13,9 +13,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Notifications; using osu.Game.Resources.Localisation.Web; @@ -27,7 +29,7 @@ namespace osu.Game.Overlays { public partial class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, INotificationOverlay { - public string IconTexture => "Icons/Hexacons/notification"; + public IconUsage Icon => HexaconsIcons.Notification; public LocalisableString Title => NotificationsStrings.HeaderTitle; public LocalisableString Description => NotificationsStrings.HeaderDescription; diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 5bbf18a959..425ff0935d 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays { public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { - public string IconTexture => "Icons/Hexacons/music"; + public IconUsage Icon => HexaconsIcons.Music; public LocalisableString Title => NowPlayingStrings.HeaderTitle; public LocalisableString Description => NowPlayingStrings.HeaderDescription; diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs index 1d207e5f7d..a2ff7032b5 100644 --- a/osu.Game/Overlays/OverlayTitle.cs +++ b/osu.Game/Overlays/OverlayTitle.cs @@ -1,13 +1,9 @@ // 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.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -20,7 +16,7 @@ namespace osu.Game.Overlays public const float ICON_SIZE = 30; private readonly OsuSpriteText titleText; - private readonly Container icon; + private readonly Container iconContainer; private LocalisableString title; @@ -32,12 +28,20 @@ namespace osu.Game.Overlays public LocalisableString Description { get; protected set; } - private string iconTexture; + private IconUsage icon; - public string IconTexture + public IconUsage Icon { - get => iconTexture; - protected set => icon.Child = new OverlayTitleIcon(iconTexture = value); + get => icon; + protected set => iconContainer.Child = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, + + Icon = icon = value, + }; } protected OverlayTitle() @@ -51,7 +55,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Horizontal, Children = new Drawable[] { - icon = new Container + iconContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -68,26 +72,5 @@ namespace osu.Game.Overlays } }; } - - private partial class OverlayTitleIcon : Sprite - { - private readonly string textureName; - - public OverlayTitleIcon(string textureName) - { - this.textureName = textureName; - - RelativeSizeAxes = Axes.Both; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - FillMode = FillMode.Fit; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - Texture = textures.Get(textureName); - } - } } } diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 80d48ae09e..78343d08f1 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Overlays.Profile.Header; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Resources.Localisation.Web; @@ -86,7 +87,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeaderTitle() { Title = PageTitleStrings.MainUsersControllerDefault; - IconTexture = "Icons/Hexacons/profile"; + Icon = HexaconsIcons.Profile; } } } diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index 44f278a237..63128fb73d 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Game.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Rulesets; using osu.Game.Users; @@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Rankings { Title = PageTitleStrings.MainRankingControllerDefault; Description = NamedOverlayComponentStrings.RankingsDescription; - IconTexture = "Icons/Hexacons/rankings"; + Icon = HexaconsIcons.Rankings; } } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 291281124c..746d451343 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -12,14 +12,16 @@ using osu.Game.Overlays.Settings.Sections.Input; using osuTK.Graphics; using System.Collections.Generic; using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; namespace osu.Game.Overlays { public partial class SettingsOverlay : SettingsPanel, INamedOverlayComponent { - public string IconTexture => "Icons/Hexacons/settings"; + public IconUsage Icon => HexaconsIcons.Settings; public LocalisableString Title => SettingsStrings.HeaderTitle; public LocalisableString Description => SettingsStrings.HeaderDescription; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index e181322dda..08bcb6bd8a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -10,12 +10,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Database; using osu.Framework.Localisation; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -36,16 +35,13 @@ namespace osu.Game.Overlays.Toolbar IconContainer.Show(); } - [Resolved] - private TextureStore textures { get; set; } = null!; - [Resolved] private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!; - public void SetIcon(string texture) => - SetIcon(new Sprite + public void SetIcon(IconUsage icon) => + SetIcon(new SpriteIcon { - Texture = textures.Get(texture), + Icon = icon, }); public LocalisableString Text diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index ba2c8282c5..ded0229d67 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Graphics; using osu.Game.Input.Bindings; using osu.Game.Localisation; @@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Toolbar { TooltipMain = ToolbarStrings.HomeHeaderTitle; TooltipSub = ToolbarStrings.HomeHeaderDescription; - SetIcon("Icons/Hexacons/home"); + SetIcon(HexaconsIcons.Home); } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 7bd48174db..78c976111b 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Toolbar { TooltipMain = named.Title; TooltipSub = named.Description; - SetIcon(named.IconTexture); + SetIcon(named.Icon); } } } diff --git a/osu.Game/Overlays/Wiki/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index 9317813fc4..9e9e565684 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -8,6 +8,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; +using osu.Game.Graphics; using osu.Game.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Resources.Localisation.Web; @@ -81,7 +82,7 @@ namespace osu.Game.Overlays.Wiki { Title = PageTitleStrings.MainWikiControllerDefault; Description = NamedOverlayComponentStrings.WikiDescription; - IconTexture = "Icons/Hexacons/wiki"; + Icon = HexaconsIcons.Wiki; } } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index a67375b0a4..5c77672d90 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -46,12 +46,12 @@ namespace osu.Game.Screens.Edit.Components.Menus Padding = new MarginPadding(8), Children = new Drawable[] { - new Sprite + new SpriteIcon { Size = new Vector2(26), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Texture = textures.Get("Icons/Hexacons/editor"), + Icon = HexaconsIcons.Editor, }, text = new TextFlowContainer { diff --git a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs index 788beba9d9..93448c4394 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreenHeader.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osuTK.Graphics; @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Edit.Setup { Title = EditorSetupStrings.BeatmapSetup.ToLower(); Description = EditorSetupStrings.BeatmapSetupDescription; - IconTexture = "Icons/Hexacons/social"; + Icon = HexaconsIcons.Social; } } From 5905ca64925f4cf4b3ce7d07974e39a353df18e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 10:56:59 +0900 Subject: [PATCH 851/896] Add second level menu for skin editors --- .../TestSceneBeatmapEditorNavigation.cs | 2 +- .../UserInterface/TestSceneButtonSystem.cs | 2 +- osu.Game/Screens/Menu/ButtonSystem.cs | 19 +++++++++++++++++-- osu.Game/Screens/Menu/MainMenu.cs | 10 +++++++++- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index b79b61202b..ce266a2d77 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); - AddStep("open editor", () => Game.ChildrenOfType().Single().OnEdit.Invoke()); + AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap.Invoke()); AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); AddStep("click on file", () => { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index ac811aeb65..1de47aee69 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.UserInterface break; case Key.E: - buttons.OnEdit = action; + buttons.OnEditBeatmap = action; break; case Key.D: diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index a0cf9f5322..79739e4f0c 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -40,7 +41,8 @@ namespace osu.Game.Screens.Menu private readonly IBindable isIdle = new BindableBool(); - public Action OnEdit; + public Action OnEditBeatmap; + public Action OnEditSkin; public Action OnExit; public Action OnBeatmapListing; public Action OnSolo; @@ -84,6 +86,7 @@ namespace osu.Game.Screens.Menu private readonly List buttonsTopLevel = new List(); private readonly List buttonsPlay = new List(); + private readonly List buttonsEdit = new List(); private Sample sampleBackToLogo; private Sample sampleLogoSwoosh; @@ -105,6 +108,11 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(new Drawable[] { new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), + backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, + -WEDGE_WIDTH) + { + VisibleState = ButtonSystemState.Edit, + }, backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) { @@ -133,14 +141,19 @@ namespace osu.Game.Screens.Menu buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); + buttonsEdit.Add(new MainMenuButton(CommonStrings.Beatmaps.ToLower(), @"button-default-select", FontAwesome.Solid.User, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); + buttonsEdit.Add(new MainMenuButton(CommonStrings.Skins.ToLower(), @"button-default-select", FontAwesome.Solid.Users, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); + buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); if (host.CanExit) buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); buttonArea.AddRange(buttonsPlay); + buttonArea.AddRange(buttonsEdit); buttonArea.AddRange(buttonsTopLevel); buttonArea.ForEach(b => @@ -270,6 +283,7 @@ namespace osu.Game.Screens.Menu return true; + case ButtonSystemState.Edit: case ButtonSystemState.Play: StopSamplePlayback(); backButton.TriggerClick(); @@ -414,6 +428,7 @@ namespace osu.Game.Screens.Menu Initial, TopLevel, Play, + Edit, EnteringMode, } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 0f73707544..c25e62d69e 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -25,6 +25,7 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Overlays.SkinEditor; using osu.Game.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; @@ -93,6 +94,9 @@ namespace osu.Game.Screens.Menu private Sample reappearSampleSwoosh; + [Resolved(canBeNull: true)] + private SkinEditorOverlay skinEditor { get; set; } + [BackgroundDependencyLoader(true)] private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics, AudioManager audio) { @@ -120,11 +124,15 @@ namespace osu.Game.Screens.Menu { Buttons = new ButtonSystem { - OnEdit = delegate + OnEditBeatmap = () => { Beatmap.SetDefault(); this.Push(new EditorLoader()); }, + OnEditSkin = () => + { + skinEditor?.Show(); + }, OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), From 7e59a1d0be3eb3aae50f17e1a361b6a357286194 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:00:22 +0900 Subject: [PATCH 852/896] Apply NRT to `ButtonSystem` --- .../Editing/TestSceneOpenEditorTimestamp.cs | 2 +- .../TestSceneBeatmapEditorNavigation.cs | 2 +- osu.Game/Screens/Menu/ButtonSystem.cs | 52 +++++++++---------- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs index 2c8655a5f5..1a754d5145 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Editing () => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks), () => Is.EqualTo(1)); - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); addStepClickLink("00:00:000 (1)", waitForSeek: false); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index ce266a2d77..9930349b1b 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tests.Visual.Navigation { AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType().SingleOrDefault() != null); - AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap.Invoke()); + AddStep("open editor", () => Game.ChildrenOfType().Single().OnEditBeatmap?.Invoke()); AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded); AddStep("click on file", () => { diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 79739e4f0c..c4c7599fd7 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -1,12 +1,9 @@ // 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 JetBrains.Annotations; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -37,24 +34,23 @@ namespace osu.Game.Screens.Menu { public partial class ButtonSystem : Container, IStateful, IKeyBindingHandler { - public event Action StateChanged; - - private readonly IBindable isIdle = new BindableBool(); - - public Action OnEditBeatmap; - public Action OnEditSkin; - public Action OnExit; - public Action OnBeatmapListing; - public Action OnSolo; - public Action OnSettings; - public Action OnMultiplayer; - public Action OnPlaylists; - public const float BUTTON_WIDTH = 140f; public const float WEDGE_WIDTH = 20; - [CanBeNull] - private OsuLogo logo; + public event Action? StateChanged; + + public Action? OnEditBeatmap; + public Action? OnEditSkin; + public Action? OnExit; + public Action? OnBeatmapListing; + public Action? OnSolo; + public Action? OnSettings; + public Action? OnMultiplayer; + public Action? OnPlaylists; + + private readonly IBindable isIdle = new BindableBool(); + + private OsuLogo? logo; /// /// Assign the that this ButtonSystem should manage the position of. @@ -88,8 +84,8 @@ namespace osu.Game.Screens.Menu private readonly List buttonsPlay = new List(); private readonly List buttonsEdit = new List(); - private Sample sampleBackToLogo; - private Sample sampleLogoSwoosh; + private Sample? sampleBackToLogo; + private Sample? sampleLogoSwoosh; private readonly LogoTrackingContainer logoTrackingContainer; @@ -124,17 +120,17 @@ namespace osu.Game.Screens.Menu buttonArea.Flow.CentreTarget = logoTrackingContainer.LogoFacade; } - [Resolved(CanBeNull = true)] - private OsuGame game { get; set; } + [Resolved] + private IAPIProvider api { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private OsuGame? game { get; set; } - [Resolved(CanBeNull = true)] - private LoginOverlay loginOverlay { get; set; } + [Resolved] + private LoginOverlay? loginOverlay { get; set; } - [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) + [BackgroundDependencyLoader] + private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) { buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); @@ -354,7 +350,7 @@ namespace osu.Game.Screens.Menu } } - private ScheduledDelegate logoDelayedAction; + private ScheduledDelegate? logoDelayedAction; private void updateLogoState(ButtonSystemState lastState = ButtonSystemState.Initial) { From 8ad414488a630787453fe953bb1c70d327daa992 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:02:30 +0900 Subject: [PATCH 853/896] Play out previous transforms immediately to avoid flow issues with multiple sub menus --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index c4c7599fd7..a954fb50b7 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -338,6 +338,8 @@ namespace osu.Game.Screens.Menu Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}"); + buttonArea.FinishTransforms(true); + using (buttonArea.BeginDelayedSequence(lastState == ButtonSystemState.Initial ? 150 : 0)) { buttonArea.ButtonSystemState = state; From 1d1b3ca9826127ad986bfd3afc35810622602406 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:05:57 +0900 Subject: [PATCH 854/896] Apply NRT to `MainMenuButton` --- osu.Game/Screens/Menu/MainMenuButton.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index 63fc34b4fb..daf8451899 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.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; using System.Linq; using osu.Framework; @@ -33,7 +31,7 @@ namespace osu.Game.Screens.Menu /// public partial class MainMenuButton : BeatSyncedContainer, IStateful { - public event Action StateChanged; + public event Action? StateChanged; public readonly Key[] TriggerKeys; @@ -48,14 +46,14 @@ namespace osu.Game.Screens.Menu /// public ButtonSystemState VisibleState = ButtonSystemState.TopLevel; - private readonly Action clickAction; - private Sample sampleClick; - private Sample sampleHover; - private SampleChannel sampleChannel; + private readonly Action? clickAction; + private Sample? sampleClick; + private Sample? sampleHover; + private SampleChannel? sampleChannel; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); - public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, params Key[] triggerKeys) + public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, float extraWidth = 0, params Key[] triggerKeys) { this.sampleName = sampleName; this.clickAction = clickAction; From a069a673fa1866e06451ba620daa2191df0e8403 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:15:18 +0900 Subject: [PATCH 855/896] Allow buttons to be displayed on more than one state (and share the back button) --- osu.Game/Screens/Menu/ButtonSystem.cs | 8 ++------ osu.Game/Screens/Menu/MainMenuButton.cs | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index a954fb50b7..995cdaf450 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -107,12 +107,8 @@ namespace osu.Game.Screens.Menu backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) { - VisibleState = ButtonSystemState.Edit, - }, - backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, - -WEDGE_WIDTH) - { - VisibleState = ButtonSystemState.Play, + VisibleStateMin = ButtonSystemState.Play, + VisibleStateMax = ButtonSystemState.Edit, }, logoTrackingContainer.LogoFacade.With(d => d.Scale = new Vector2(0.74f)) }); diff --git a/osu.Game/Screens/Menu/MainMenuButton.cs b/osu.Game/Screens/Menu/MainMenuButton.cs index daf8451899..bc638b44ac 100644 --- a/osu.Game/Screens/Menu/MainMenuButton.cs +++ b/osu.Game/Screens/Menu/MainMenuButton.cs @@ -42,9 +42,19 @@ namespace osu.Game.Screens.Menu private readonly string sampleName; /// - /// The menu state for which we are visible for. + /// The menu state for which we are visible for (assuming only one). /// - public ButtonSystemState VisibleState = ButtonSystemState.TopLevel; + public ButtonSystemState VisibleState + { + set + { + VisibleStateMin = value; + VisibleStateMax = value; + } + } + + public ButtonSystemState VisibleStateMin = ButtonSystemState.TopLevel; + public ButtonSystemState VisibleStateMax = ButtonSystemState.TopLevel; private readonly Action? clickAction; private Sample? sampleClick; @@ -313,9 +323,9 @@ namespace osu.Game.Screens.Menu break; default: - if (value == VisibleState) + if (value <= VisibleStateMax && value >= VisibleStateMin) State = ButtonState.Expanded; - else if (value < VisibleState) + else if (value < VisibleStateMin) State = ButtonState.Contracted; else State = ButtonState.Exploded; From a02b6a5bcb23af26a6fee502a9423b517f75b112 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 11:28:06 +0900 Subject: [PATCH 856/896] Update menu key shortcut test coverage --- .../UserInterface/TestSceneButtonSystem.cs | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs index 1de47aee69..8f72be37df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneButtonSystem.cs @@ -67,14 +67,15 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Enter mode", performEnterMode); } - [TestCase(Key.P, true)] - [TestCase(Key.M, true)] - [TestCase(Key.L, true)] - [TestCase(Key.E, false)] - [TestCase(Key.D, false)] - [TestCase(Key.Q, false)] - [TestCase(Key.O, false)] - public void TestShortcutKeys(Key key, bool entersPlay) + [TestCase(Key.P, Key.P)] + [TestCase(Key.M, Key.P)] + [TestCase(Key.L, Key.P)] + [TestCase(Key.B, Key.E)] + [TestCase(Key.S, Key.E)] + [TestCase(Key.D, null)] + [TestCase(Key.Q, null)] + [TestCase(Key.O, null)] + public void TestShortcutKeys(Key key, Key? subMenuEnterKey) { int activationCount = -1; AddStep("set up action", () => @@ -96,10 +97,14 @@ namespace osu.Game.Tests.Visual.UserInterface buttons.OnPlaylists = action; break; - case Key.E: + case Key.B: buttons.OnEditBeatmap = action; break; + case Key.S: + buttons.OnEditSkin = action; + break; + case Key.D: buttons.OnBeatmapListing = action; break; @@ -117,10 +122,10 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep($"press {key}", () => InputManager.Key(key)); AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel); - if (entersPlay) + if (subMenuEnterKey != null) { - AddStep("press P", () => InputManager.Key(Key.P)); - AddAssert("state is play", () => buttons.State == ButtonSystemState.Play); + AddStep($"press {subMenuEnterKey}", () => InputManager.Key(subMenuEnterKey.Value)); + AddAssert("state is not top menu", () => buttons.State != ButtonSystemState.TopLevel); } AddStep($"press {key}", () => InputManager.Key(key)); From c7f1fd23e76170ee9c52973022ff0d630b69917f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 24 Nov 2023 06:09:41 +0300 Subject: [PATCH 857/896] Implement spinner glow --- .../Skinning/Argon/ArgonSpinnerProgressArc.cs | 122 +++++++++++++++++- 1 file changed, 120 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs index 31cdc0dc0f..b719138d33 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs @@ -2,10 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime.InteropServices; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Shaders.Types; +using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; @@ -19,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon private const float arc_fill = 0.15f; private const float arc_radius = 0.12f; - private CircularProgress fill = null!; + private ProgressFill fill = null!; private DrawableSpinner spinner = null!; @@ -45,13 +52,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon InnerRadius = arc_radius, RoundedCaps = true, }, - fill = new CircularProgress + fill = new ProgressFill { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, InnerRadius = arc_radius, RoundedCaps = true, + GlowColour = new Color4(171, 255, 255, 255) } }; } @@ -67,5 +75,115 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon fill.Rotation = (float)(90 - fill.Current.Value * 180); } + + private partial class ProgressFill : CircularProgress + { + private Color4 glowColour = Color4.White; + + public Color4 GlowColour + { + get => glowColour; + set + { + glowColour = value; + Invalidate(Invalidation.DrawNode); + } + } + + private Texture glowTexture = null!; + private IShader glowShader = null!; + private float glowSize; + + [BackgroundDependencyLoader] + private void load(TextureStore textures, ShaderManager shaders) + { + glowTexture = textures.Get("Gameplay/osu/spinner-glow"); + glowShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "SpinnerGlow"); + glowSize = Blur.KernelSize(50); // Half of the maximum blur sigma in the design (which is 100) + } + + protected override DrawNode CreateDrawNode() => new ProgressFillDrawNode(this); + + private class ProgressFillDrawNode : CircularProgressDrawNode + { + protected new ProgressFill Source => (ProgressFill)base.Source; + + public ProgressFillDrawNode(CircularProgress source) + : base(source) + { + } + + private Texture glowTexture = null!; + private IShader glowShader = null!; + private Quad glowQuad; + private float relativeGlowSize; + private Color4 glowColour; + + public override void ApplyState() + { + base.ApplyState(); + + glowTexture = Source.glowTexture; + glowShader = Source.glowShader; + glowColour = Source.glowColour; + + // Inflated draw quad by the size of the blur. + glowQuad = Source.ToScreenSpace(DrawRectangle.Inflate(Source.glowSize)); + relativeGlowSize = Source.glowSize / Source.DrawSize.X; + } + + public override void Draw(IRenderer renderer) + { + base.Draw(renderer); + drawGlow(renderer); + } + + private void drawGlow(IRenderer renderer) + { + renderer.SetBlend(BlendingParameters.Additive); + + glowShader.Bind(); + bindGlowUniformResources(glowShader, renderer); + + ColourInfo col = DrawColourInfo.Colour; + col.ApplyChild(glowColour); + + renderer.DrawQuad(glowTexture, glowQuad, col); + + glowShader.Unbind(); + } + + private IUniformBuffer? progressGlowParametersBuffer; + + private void bindGlowUniformResources(IShader shader, IRenderer renderer) + { + progressGlowParametersBuffer ??= renderer.CreateUniformBuffer(); + progressGlowParametersBuffer.Data = new ProgressGlowParameters + { + InnerRadius = InnerRadius, + Progress = Progress, + TexelSize = TexelSize, + GlowSize = relativeGlowSize + }; + + shader.BindUniformBlock("m_ProgressGlowParameters", progressGlowParametersBuffer); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + progressGlowParametersBuffer?.Dispose(); + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private record struct ProgressGlowParameters + { + public UniformFloat InnerRadius; + public UniformFloat Progress; + public UniformFloat TexelSize; + public UniformFloat GlowSize; + } + } + } } } From b8179aa875dcb27746bf1e9f236117c3f9bdb30c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 13:23:35 +0900 Subject: [PATCH 858/896] Use better(?) icons and full strings --- osu.Game/Localisation/EditorStrings.cs | 5 +++++ osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index b20b5bc65a..6ad12f54df 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -9,6 +9,11 @@ namespace osu.Game.Localisation { private const string prefix = @"osu.Game.Resources.Localisation.Editor"; + /// + /// "Beatmap editor" + /// + public static LocalisableString BeatmapEditor => new TranslatableString(getKey(@"beatmap_editor"), @"Beatmap editor"); + /// /// "Waveform opacity" /// diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 995cdaf450..8173375c29 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -133,8 +133,8 @@ namespace osu.Game.Screens.Menu buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); - buttonsEdit.Add(new MainMenuButton(CommonStrings.Beatmaps.ToLower(), @"button-default-select", FontAwesome.Solid.User, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); - buttonsEdit.Add(new MainMenuButton(CommonStrings.Skins.ToLower(), @"button-default-select", FontAwesome.Solid.Users, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); + buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", HexaconsIcons.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); + buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", HexaconsIcons.Editor, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); From 62a04a93c837db0aef81557585120877f513660c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 23 Nov 2023 16:22:34 +0900 Subject: [PATCH 859/896] Implement legacy catch health processor --- .../Scoring/CatchHealthProcessor.cs | 15 ++ .../Scoring/LegacyCatchHealthProcessor.cs | 237 ++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs create mode 100644 osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs new file mode 100644 index 0000000000..7e8162bdfa --- /dev/null +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -0,0 +1,15 @@ +// 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.Scoring; + +namespace osu.Game.Rulesets.Catch.Scoring +{ + public partial class CatchHealthProcessor : DrainingHealthProcessor + { + public CatchHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs new file mode 100644 index 0000000000..ef13ba222c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs @@ -0,0 +1,237 @@ +// 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.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Catch.Scoring +{ + /// + /// Reference implementation for osu!stable's HP drain. + /// Cannot be used for gameplay. + /// + public partial class LegacyCatchHealthProcessor : DrainingHealthProcessor + { + private const double hp_bar_maximum = 200; + private const double hp_combo_geki = 14; + private const double hp_hit_300 = 6; + private const double hp_slider_tick = 3; + + public Action? OnIterationFail; + public Action? OnIterationSuccess; + public bool ApplyComboEndBonus { get; set; } = true; + + private double lowestHpEver; + private double lowestHpEnd; + private double lowestHpComboEnd; + private double hpRecoveryAvailable; + private double hpMultiplierNormal; + private double hpMultiplierComboEnd; + + public LegacyCatchHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60); + lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void ApplyResultInternal(JudgementResult result) + { + if (!IsSimulating) + throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); + } + + protected override void RevertResultInternal(JudgementResult result) + { + if (!IsSimulating) + throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); + } + + protected override void Reset(bool storeResults) + { + hpMultiplierNormal = 1; + hpMultiplierComboEnd = 1; + + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.05; + double currentHp; + double currentHpUncapped; + + List<(HitObject hitObject, bool newCombo)> allObjects = enumerateHitObjects(Beatmap).Where(h => h.hitObject is Fruit || h.hitObject is Droplet || h.hitObject is Banana).ToList(); + + do + { + currentHp = hp_bar_maximum; + currentHpUncapped = hp_bar_maximum; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + int comboTooLowCount = 0; + string failReason = string.Empty; + + for (int i = 0; i < allObjects.Count; i++) + { + HitObject h = allObjects[i].hitObject; + + // Find active break (between current and lastTime) + double localLastTime = lastTime; + double breakTime = 0; + + // Subtract any break time from the duration since the last object + if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + { + BreakPeriod e = Beatmap.Breaks[currentBreak]; + + if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) + { + // consider break start equal to object end time for version 8+ since drain stops during this time + breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; + currentBreak++; + } + } + + reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})"; + break; + } + + switch (h) + { + case Fruit: + if (ApplyComboEndBonus && (i == allObjects.Count - 1 || allObjects[i + 1].newCombo)) + { + increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); + + if (currentHp < lowestHpComboEnd) + { + if (++comboTooLowCount > 2) + { + hpMultiplierComboEnd *= 1.07; + hpMultiplierNormal *= 1.03; + fail = true; + failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})"; + } + } + } + else + increaseHp(hpMultiplierNormal * hp_hit_300); + + break; + + case Banana: + increaseHp(hpMultiplierNormal / 2); + break; + + case TinyDroplet: + increaseHp(hpMultiplierNormal * hp_slider_tick * 0.1); + break; + + case Droplet: + increaseHp(hpMultiplierNormal * hp_slider_tick); + break; + } + + if (fail) + break; + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + hpMultiplierComboEnd *= 1.01; + hpMultiplierNormal *= 1.01; + failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})"; + } + + double recovery = (currentHpUncapped - hp_bar_maximum) / allObjects.Count; + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + hpMultiplierComboEnd *= 1.02; + hpMultiplierNormal *= 1.01; + failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})"; + } + + if (fail) + { + OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); + continue; + } + + OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}"); + return testDrop / hp_bar_maximum; + } while (true); + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(double amount) + { + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount)); + } + } + + private IEnumerable<(HitObject hitObject, bool newCombo)> enumerateHitObjects(IBeatmap beatmap) + { + return enumerateRecursively(beatmap.HitObjects); + + static IEnumerable<(HitObject hitObject, bool newCombo)> enumerateRecursively(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + // The combo end will either be attached to the hitobject itself if it has no children, or the very first child if it has children. + bool newCombo = (hitObject as IHasComboInformation)?.NewCombo ?? false; + + foreach ((HitObject nested, bool _) in enumerateRecursively(hitObject.NestedHitObjects)) + { + yield return (nested, newCombo); + + // Since the combo was attached to the first child, don't attach it to any other child or the parenting hitobject itself. + newCombo = false; + } + + yield return (hitObject, newCombo); + } + } + } + } +} From acf3de5e25f343f492bbf33135bb2160126a94e6 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 13:22:46 +0900 Subject: [PATCH 860/896] Add CatchHealthProcessor, following legacy calculations --- .../Scoring/CatchHealthProcessor.cs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 7e8162bdfa..1f6a696f98 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -1,15 +1,175 @@ // 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.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { public partial class CatchHealthProcessor : DrainingHealthProcessor { + public Action? OnIterationFail; + public Action? OnIterationSuccess; + + private double lowestHpEver; + private double lowestHpEnd; + private double hpRecoveryAvailable; + private double hpMultiplierNormal; + public CatchHealthProcessor(double drainStartTime) : base(drainStartTime) { } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void Reset(bool storeResults) + { + hpMultiplierNormal = 1; + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.00025; + double currentHp; + double currentHpUncapped; + + while (true) + { + currentHp = 1; + currentHpUncapped = 1; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + + List allObjects = EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana).ToList(); + + for (int i = 0; i < allObjects.Count; i++) + { + HitObject h = allObjects[i]; + + double localLastTime = lastTime; + double breakTime = 0; + + if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + { + BreakPeriod e = Beatmap.Breaks[currentBreak]; + + if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) + { + // consider break start equal to object end time for version 8+ since drain stops during this time + breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; + currentBreak++; + } + } + + reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); + break; + } + + increaseHp(h); + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + hpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); + } + + double recovery = (currentHpUncapped - 1) / allObjects.Count; + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + hpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); + } + + if (!fail) + { + OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); + return testDrop; + } + } + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(HitObject hitObject) + { + double amount = healthIncreaseFor(hitObject.CreateJudgement().MaxResult); + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); + } + } + + protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.Type); + + private double healthIncreaseFor(HitResult result) + { + double increase = 0; + + switch (result) + { + case HitResult.SmallTickMiss: + return 0; + + case HitResult.LargeTickMiss: + case HitResult.Miss: + return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2); + + case HitResult.SmallTickHit: + increase = 0.0015; + break; + + case HitResult.LargeTickHit: + increase = 0.015; + break; + + case HitResult.Great: + increase = 0.03; + break; + + case HitResult.LargeBonus: + increase = 0.0025; + break; + } + + return hpMultiplierNormal * increase; + } } } From 4ba6450c7703bbd44e97a5b3180a5afc4a5115ab Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 13:32:14 +0900 Subject: [PATCH 861/896] Use better break calculation --- .../Scoring/CatchHealthProcessor.cs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 1f6a696f98..6d831ad223 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -65,22 +64,16 @@ namespace osu.Game.Rulesets.Catch.Scoring { HitObject h = allObjects[i]; - double localLastTime = lastTime; - double breakTime = 0; - - if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) { - BreakPeriod e = Beatmap.Breaks[currentBreak]; - - if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) - { - // consider break start equal to object end time for version 8+ since drain stops during this time - breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; - currentBreak++; - } + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = h.StartTime; + currentBreak++; } - reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); + reduceHp(testDrop * (h.StartTime - lastTime)); lastTime = h.GetEndTime(); From bb662676347c79343da3ba480693ff5c272b6878 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 13:46:41 +0900 Subject: [PATCH 862/896] Actually use CatchHealthProcessor for the ruleset --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9ceb78893e..013a709663 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Catch public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new CatchHealthProcessor(drainStartTime); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap); From 4c7d2bb0fbae426e533572ad905531677f3f27fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 15:14:25 +0900 Subject: [PATCH 863/896] Apply NRT to `SpectatorScreen` --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 31 +++++++++----------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 48b5c210b8..f9d5355663 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -1,13 +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.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; @@ -33,22 +30,27 @@ namespace osu.Game.Screens.Spectate private readonly List users = new List(); [Resolved] - private BeatmapManager beatmaps { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private RulesetStore rulesets { get; set; } + private RulesetStore rulesets { get; set; } = null!; [Resolved] - private SpectatorClient spectatorClient { get; set; } + private SpectatorClient spectatorClient { get; set; } = null!; [Resolved] - private UserLookupCache userLookupCache { get; set; } + private UserLookupCache userLookupCache { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; private readonly IBindableDictionary userStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); + private IDisposable? realmSubscription; + /// /// Creates a new . /// @@ -58,11 +60,6 @@ namespace osu.Game.Screens.Spectate this.users.AddRange(users); } - [Resolved] - private RealmAccess realm { get; set; } - - private IDisposable realmSubscription; - protected override void LoadComplete() { base.LoadComplete(); @@ -90,7 +87,7 @@ namespace osu.Game.Screens.Spectate })); } - private void beatmapsChanged(IRealmCollection items, ChangeSet changes) + private void beatmapsChanged(IRealmCollection items, ChangeSet? changes) { if (changes?.InsertedIndices == null) return; @@ -109,7 +106,7 @@ namespace osu.Game.Screens.Spectate } } - private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs e) + private void onUserStatesChanged(object? sender, NotifyDictionaryChangedEventArgs e) { switch (e.Action) { @@ -207,14 +204,14 @@ namespace osu.Game.Screens.Spectate ///
/// The user whose state has changed. /// The new state. - protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState); + protected abstract void OnNewPlayingUserState(int userId, SpectatorState spectatorState); /// /// Starts gameplay for a user. /// /// The user to start gameplay for. /// The gameplay state. - protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState); + protected abstract void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState); /// /// Quits gameplay for a user. @@ -243,7 +240,7 @@ namespace osu.Game.Screens.Spectate { base.Dispose(isDisposing); - if (spectatorClient != null) + if (spectatorClient.IsNotNull()) { foreach ((int userId, var _) in userMap) spectatorClient.StopWatchingUser(userId); From dabbdf674bc7cab4bbb414ef451c2af158ff8be4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 15:19:04 +0900 Subject: [PATCH 864/896] Rename `SoloSpectator` to `SoloSpectatorScreen` --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 8 ++++---- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- .../Play/{SoloSpectator.cs => SoloSpectatorScreen.cs} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Screens/Play/{SoloSpectator.cs => SoloSpectatorScreen.cs} (98%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index ffd034e4d2..db74f7d673 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestSpectatorClient spectatorClient => dependenciesScreen.SpectatorClient; private DependenciesScreen dependenciesScreen; - private SoloSpectator spectatorScreen; + private SoloSpectatorScreen spectatorScreen; private BeatmapSetInfo importedBeatmap; private int importedBeatmapId; @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator); + AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectatorScreen); start(); waitForPlayer(); @@ -255,7 +255,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(-1234); sendFrames(); - AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); + AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectatorScreen); } [Test] @@ -381,7 +381,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadSpectatingScreen() { - AddStep("load spectator", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser))); + AddStep("load spectator", () => LoadScreen(spectatorScreen = new SoloSpectatorScreen(streamingUser))); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 02f0a6e80d..6967a61204 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -212,7 +212,7 @@ namespace osu.Game.Overlays.Dashboard Text = "Spectate", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectator(User))), + Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))), Enabled = { Value = User.Id != api.LocalUser.Value.Id } } } diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs similarity index 98% rename from osu.Game/Screens/Play/SoloSpectator.cs rename to osu.Game/Screens/Play/SoloSpectatorScreen.cs index f5af2684d3..59b7d87129 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -33,7 +33,7 @@ using osuTK; namespace osu.Game.Screens.Play { [Cached(typeof(IPreviewTrackOwner))] - public partial class SoloSpectator : SpectatorScreen, IPreviewTrackOwner + public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner { [NotNull] private readonly APIUser targetUser; @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Play private APIBeatmapSet beatmapSet; - public SoloSpectator([NotNull] APIUser targetUser) + public SoloSpectatorScreen([NotNull] APIUser targetUser) : base(targetUser.Id) { this.targetUser = targetUser; From 335e8efff769c4ccd5cb4b9c66687a1cc16f941d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 15:56:49 +0900 Subject: [PATCH 865/896] Apply NRT to `SoloSpecatorScreen` --- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 38 ++++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 59b7d87129..7acdc51fb3 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -1,10 +1,7 @@ // 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; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -35,39 +32,38 @@ namespace osu.Game.Screens.Play [Cached(typeof(IPreviewTrackOwner))] public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner { - [NotNull] - private readonly APIUser targetUser; + [Resolved] + private IAPIProvider api { get; set; } = null!; [Resolved] - private IAPIProvider api { get; set; } + private PreviewTrackManager previewTrackManager { get; set; } = null!; [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } + private BeatmapManager beatmaps { get; set; } = null!; [Resolved] - private BeatmapManager beatmaps { get; set; } - - [Resolved] - private BeatmapModelDownloader beatmapDownloader { get; set; } + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private Container beatmapPanelContainer; - private RoundedButton watchButton; - private SettingsCheckbox automaticDownload; + private Container beatmapPanelContainer = null!; + private RoundedButton watchButton = null!; + private SettingsCheckbox automaticDownload = null!; + + private readonly APIUser targetUser; /// /// The player's immediate online gameplay state. /// This doesn't always reflect the gameplay state being watched. /// - private SpectatorGameplayState immediateSpectatorGameplayState; + private SpectatorGameplayState? immediateSpectatorGameplayState; - private GetBeatmapSetRequest onlineBeatmapRequest; + private GetBeatmapSetRequest? onlineBeatmapRequest; - private APIBeatmapSet beatmapSet; + private APIBeatmapSet? beatmapSet; - public SoloSpectatorScreen([NotNull] APIUser targetUser) + public SoloSpectatorScreen(APIUser targetUser) : base(targetUser.Id) { this.targetUser = targetUser; @@ -199,10 +195,12 @@ namespace osu.Game.Screens.Play previewTrackManager.StopAnyPlaying(this); } - private ScheduledDelegate scheduledStart; + private ScheduledDelegate? scheduledStart; - private void scheduleStart(SpectatorGameplayState spectatorGameplayState) + private void scheduleStart(SpectatorGameplayState? spectatorGameplayState) { + Debug.Assert(spectatorGameplayState != null); + // This function may be called multiple times in quick succession once the screen becomes current again. scheduledStart?.Cancel(); scheduledStart = Schedule(() => From e06d79281dc682fadf83cac72bd0bfc2561e3e65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 16:40:22 +0900 Subject: [PATCH 866/896] Update tests to work with nested screen --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index db74f7d673..9a1fd660d5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -100,19 +100,18 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - AddUntilStep("wait for player loader", () => (Stack.CurrentScreen as PlayerLoader)?.IsLoaded == true); + AddUntilStep("wait for player loader", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); AddUntilStep("queue send frames on player load", () => { - var loadingPlayer = (Stack.CurrentScreen as PlayerLoader)?.CurrentPlayer; + var loadingPlayer = this.ChildrenOfType().SingleOrDefault()?.CurrentPlayer; if (loadingPlayer == null) return false; loadingPlayer.OnLoadComplete += _ => - { spectatorClient.SendFramesFromUser(streamingUser.Id, 10, gameplay_start); - }; + return true; }); @@ -360,12 +359,12 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; - private Player player => Stack.CurrentScreen as Player; + private Player player => this.ChildrenOfType().Single(); private double currentFrameStableTime => player.ChildrenOfType().First().CurrentTime; - private void waitForPlayer() => AddUntilStep("wait for player", () => (Stack.CurrentScreen as Player)?.IsLoaded == true); + private void waitForPlayer() => AddUntilStep("wait for player", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.SendStartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); From ed5375536f1cf96446617b68251354a7f458c2c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 17:14:07 +0900 Subject: [PATCH 867/896] Reduce access to various fields and events in `SpectatorClient` Restore some `virtual` specs to appease Moq --- osu.Game/Online/Spectator/SpectatorClient.cs | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 14e137caf1..e7435adf29 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -46,9 +46,9 @@ namespace osu.Game.Online.Spectator public IBindableList PlayingUsers => playingUsers; /// - /// Whether the local user is playing. + /// Whether the spectated user is playing. /// - protected internal bool IsPlaying { get; private set; } + private bool isPlaying { get; set; } /// /// Called whenever new frames arrive from the server. @@ -58,17 +58,17 @@ namespace osu.Game.Online.Spectator /// /// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session. /// - public virtual event Action? OnUserBeganPlaying; + public event Action? OnUserBeganPlaying; /// /// Called whenever a user finishes a play session. /// - public virtual event Action? OnUserFinishedPlaying; + public event Action? OnUserFinishedPlaying; /// /// Called whenever a user-submitted score has been fully processed. /// - public virtual event Action? OnUserScoreProcessed; + public event Action? OnUserScoreProcessed; /// /// A dictionary containing all users currently being watched, with the number of watching components for each user. @@ -114,7 +114,7 @@ namespace osu.Game.Online.Spectator } // re-send state in case it wasn't received - if (IsPlaying) + if (isPlaying) // TODO: this is likely sent out of order after a reconnect scenario. needs further consideration. BeginPlayingInternal(currentScoreToken, currentState); } @@ -179,10 +179,10 @@ namespace osu.Game.Online.Spectator // This schedule is only here to match the one below in `EndPlaying`. Schedule(() => { - if (IsPlaying) + if (isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); - IsPlaying = true; + isPlaying = true; // transfer state at point of beginning play currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID; @@ -202,7 +202,7 @@ namespace osu.Game.Online.Spectator public void HandleFrame(ReplayFrame frame) => Schedule(() => { - if (!IsPlaying) + if (!isPlaying) { Logger.Log($"Frames arrived at {nameof(SpectatorClient)} outside of gameplay scope and will be ignored."); return; @@ -224,7 +224,7 @@ namespace osu.Game.Online.Spectator // We probably need to find a better way to handle this... Schedule(() => { - if (!IsPlaying) + if (!isPlaying) return; // Disposal can take some time, leading to EndPlaying potentially being called after a future play session. @@ -235,7 +235,7 @@ namespace osu.Game.Online.Spectator if (pendingFrames.Count > 0) purgePendingFrames(); - IsPlaying = false; + isPlaying = false; currentBeatmap = null; if (state.HasPassed) From 73ad92189b0b8026ec966a8051d72d04637f8ead Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 12:07:01 +0900 Subject: [PATCH 868/896] Add test coverage of remote player quitting immediately quitting locally --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 9a1fd660d5..e3a10c655a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -332,6 +332,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send quit", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); + AddAssert("wait for player exit", () => Stack.CurrentScreen is SoloSpectatorScreen); + start(); sendFrames(); waitForPlayer(); From b024065857bbfbf2953153d3ccc2ee90171f8e52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 14:21:52 +0900 Subject: [PATCH 869/896] Remove implicit schedule of `abstract` methods in `SpectatorScreen` This allows each implementation to have control over scheduling. Without this, the solo implementation would not be able to handle quit events while watching a player, as it would push a child (gameplay) screen to the stack where the `SpectatorScreen` would usually be. --- .../Spectate/MultiSpectatorScreen.cs | 8 +++---- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 22 ++++++++++++------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 9 +++++--- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index c1b1127542..eb2980fe1e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -228,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => Schedule(() => { var playerArea = instances.Single(i => i.UserId == userId); @@ -242,9 +242,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return; playerArea.LoadScore(spectatorGameplayState.Score); - } + }); - protected override void QuitGameplay(int userId) + protected override void QuitGameplay(int userId) => Schedule(() => { RemoveUser(userId); @@ -252,7 +252,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate instance.FadeColour(colours.Gray4, 400, Easing.OutQuint); syncManager.RemoveManagedClock(instance.SpectatorPlayerClock); - } + }); public override bool OnBackButton() { diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 7acdc51fb3..770018a0c8 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -164,27 +164,33 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) + protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState) => Schedule(() => { clearDisplay(); showBeatmapPanel(spectatorState); - } + }); - protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) => Schedule(() => { immediateSpectatorGameplayState = spectatorGameplayState; watchButton.Enabled.Value = true; scheduleStart(spectatorGameplayState); - } + }); protected override void QuitGameplay(int userId) { - scheduledStart?.Cancel(); - immediateSpectatorGameplayState = null; - watchButton.Enabled.Value = false; + // Importantly, don't schedule this call, as a child screen may be present (and will cause the schedule to not be run as expected). + this.MakeCurrent(); - clearDisplay(); + Schedule(() => + { + scheduledStart?.Cancel(); + immediateSpectatorGameplayState = null; + watchButton.Enabled.Value = false; + + clearDisplay(); + }); } private void clearDisplay() diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index f9d5355663..21f96c83f5 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Spectate switch (newState.State) { case SpectatedUserState.Playing: - Schedule(() => OnNewPlayingUserState(userId, newState)); + OnNewPlayingUserState(userId, newState); startGameplay(userId); break; @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Spectate var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; - Schedule(() => StartGameplay(userId, gameplayState)); + StartGameplay(userId, gameplayState); } /// @@ -196,11 +196,12 @@ namespace osu.Game.Screens.Spectate markReceivedAllFrames(userId); gameplayStates.Remove(userId); - Schedule(() => QuitGameplay(userId)); + QuitGameplay(userId); } /// /// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user whose state has changed. /// The new state. @@ -208,6 +209,7 @@ namespace osu.Game.Screens.Spectate /// /// Starts gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user to start gameplay for. /// The gameplay state. @@ -215,6 +217,7 @@ namespace osu.Game.Screens.Spectate /// /// Quits gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. /// /// The user to quit gameplay for. protected abstract void QuitGameplay(int userId); From 51e2ce500d67c4f634d75af59b04ba69302ef44f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 12:45:03 +0900 Subject: [PATCH 870/896] Add test coverage of remote user failing immediately failing locally --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index e3a10c655a..f5e4c5da51 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -352,6 +352,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); + AddUntilStep("wait for player to fail", () => player.GameplayState.HasFailed); + start(); sendFrames(); waitForPlayer(); From ca93fdc94b07bd1bd539d72265ea65edfe8fa6a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 17:53:35 +0900 Subject: [PATCH 871/896] Add visualisation of when a spectated player fails Create a new stack each time for isolation and safety --- .../Spectate/MultiSpectatorScreen.cs | 5 ++++ osu.Game/Screens/Play/SoloSpectatorPlayer.cs | 4 +-- osu.Game/Screens/Play/SoloSpectatorScreen.cs | 14 ++++++++++- osu.Game/Screens/Play/SpectatorPlayer.cs | 12 ++++++++- osu.Game/Screens/Spectate/SpectatorScreen.cs | 25 +++++++++++++++++++ 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index eb2980fe1e..e2159f0e3b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -244,6 +244,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate playerArea.LoadScore(spectatorGameplayState.Score); }); + protected override void FailGameplay(int userId) + { + // We probably want to visualise this in the future. + } + protected override void QuitGameplay(int userId) => Schedule(() => { RemoveUser(userId); diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs index c9d1f4acaa..8d25a0148d 100644 --- a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs @@ -17,8 +17,8 @@ namespace osu.Game.Screens.Play protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo); - public SoloSpectatorPlayer(Score score, PlayerConfiguration configuration = null) - : base(score, configuration) + public SoloSpectatorPlayer(Score score) + : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.score = score; } diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index 770018a0c8..d4a9fc4b30 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -178,6 +178,19 @@ namespace osu.Game.Screens.Play scheduleStart(spectatorGameplayState); }); + protected override void FailGameplay(int userId) + { + if (this.GetChildScreen() is SpectatorPlayer player) + player.AllowFail(); + + Schedule(() => + { + scheduledStart?.Cancel(); + immediateSpectatorGameplayState = null; + clearDisplay(); + }); + } + protected override void QuitGameplay(int userId) { // Importantly, don't schedule this call, as a child screen may be present (and will cause the schedule to not be run as expected). @@ -187,7 +200,6 @@ namespace osu.Game.Screens.Play { scheduledStart?.Cancel(); immediateSpectatorGameplayState = null; - watchButton.Enabled.Value = false; clearDisplay(); }); diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 30a5ac3741..bc95fa190c 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -25,7 +25,15 @@ namespace osu.Game.Screens.Play private readonly Score score; - protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap + protected override bool CheckModsAllowFailure() + { + if (!allowFail) + return false; + + return base.CheckModsAllowFailure(); + } + + private bool allowFail; protected SpectatorPlayer(Score score, PlayerConfiguration configuration = null) : base(configuration) @@ -123,5 +131,7 @@ namespace osu.Game.Screens.Play if (SpectatorClient != null) SpectatorClient.OnNewFrames -= userSentFrames; } + + public void AllowFail() => allowFail = true; } } diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 21f96c83f5..c4aef3c878 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -137,6 +137,10 @@ namespace osu.Game.Screens.Spectate markReceivedAllFrames(userId); break; + case SpectatedUserState.Failed: + failGameplay(userId); + break; + case SpectatedUserState.Quit: quitGameplay(userId); break; @@ -185,6 +189,20 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; } + private void failGameplay(int userId) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.ContainsKey(userId)) + return; + + markReceivedAllFrames(userId); + + gameplayStates.Remove(userId); + FailGameplay(userId); + } + private void quitGameplay(int userId) { if (!userMap.ContainsKey(userId)) @@ -222,6 +240,13 @@ namespace osu.Game.Screens.Spectate /// The user to quit gameplay for. protected abstract void QuitGameplay(int userId); + /// + /// Fails gameplay for a user. + /// Thread safety is not guaranteed – should be scheduled as required. + /// + /// The user to fail gameplay for. + protected abstract void FailGameplay(int userId); + /// /// Stops spectating a user. /// From 8375dd72d6e691ef393aecf1e2c3c418f46cd128 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 10:01:59 +0900 Subject: [PATCH 872/896] Add xmldoc to new `AllowFail` method --- osu.Game/Screens/Play/Player.cs | 7 +++++++ osu.Game/Screens/Play/SpectatorPlayer.cs | 8 ++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8c7fc551ba..ad725e1a90 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -894,6 +894,13 @@ namespace osu.Game.Screens.Play #region Fail Logic + /// + /// Invoked when gameplay has permanently failed. + /// + protected virtual void OnFail() + { + } + protected FailOverlay FailOverlay { get; private set; } private FailAnimationContainer failAnimationContainer; diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index bc95fa190c..d1404ac184 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -68,6 +68,12 @@ namespace osu.Game.Screens.Play }, true); } + /// + /// Should be called when it is apparent that the player being spectated has failed. + /// This will subsequently stop blocking the fail screen from displaying (usually done out of safety). + /// + public void AllowFail() => allowFail = true; + protected override void StartGameplay() { base.StartGameplay(); @@ -131,7 +137,5 @@ namespace osu.Game.Screens.Play if (SpectatorClient != null) SpectatorClient.OnNewFrames -= userSentFrames; } - - public void AllowFail() => allowFail = true; } } From 4ad3cb3b4952aeb09847fe4e20fd581eaf8a52a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 18:20:22 +0900 Subject: [PATCH 873/896] Submit and send failed spectator state more aggressively --- osu.Game/Screens/Play/SubmittingPlayer.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 30fecbe149..14aaa8f638 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -54,6 +54,8 @@ namespace osu.Game.Screens.Play } AddInternal(new PlayerTouchInputDetector()); + + HealthProcessor.Failed += onFail; } protected override void LoadAsyncComplete() @@ -165,10 +167,21 @@ namespace osu.Game.Screens.Play spectatorClient.BeginPlaying(token, GameplayState, Score); } + private bool onFail() + { + submitFromFailOrQuit(); + return true; + } + public override bool OnExiting(ScreenExitEvent e) { bool exiting = base.OnExiting(e); + submitFromFailOrQuit(); + return exiting; + } + private void submitFromFailOrQuit() + { if (LoadedBeatmapSuccessfully) { Task.Run(async () => @@ -177,8 +190,6 @@ namespace osu.Game.Screens.Play spectatorClient.EndPlaying(GameplayState); }).FireAndForget(); } - - return exiting; } /// From ef5dd245894e3ea4b8658929054b92276b91c4e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 10:28:15 +0900 Subject: [PATCH 874/896] Update failing test coverage and fix `onFail` being called too often --- .../Visual/Gameplay/TestScenePlayerScoreSubmission.cs | 1 - osu.Game/Screens/Play/Player.cs | 1 + osu.Game/Screens/Play/SubmittingPlayer.cs | 7 +++---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs index 1a7ea20cc0..f75a2656ef 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs @@ -179,7 +179,6 @@ namespace osu.Game.Tests.Visual.Gameplay addFakeHit(); AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); - AddStep("exit", () => Player.Exit()); AddUntilStep("wait for submission", () => Player.SubmittedScore != null); AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ad725e1a90..b469ab443c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -933,6 +933,7 @@ namespace osu.Game.Screens.Play if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) Restart(true); + OnFail(); return true; } diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs index 14aaa8f638..785164178a 100644 --- a/osu.Game/Screens/Play/SubmittingPlayer.cs +++ b/osu.Game/Screens/Play/SubmittingPlayer.cs @@ -54,8 +54,6 @@ namespace osu.Game.Screens.Play } AddInternal(new PlayerTouchInputDetector()); - - HealthProcessor.Failed += onFail; } protected override void LoadAsyncComplete() @@ -167,10 +165,11 @@ namespace osu.Game.Screens.Play spectatorClient.BeginPlaying(token, GameplayState, Score); } - private bool onFail() + protected override void OnFail() { + base.OnFail(); + submitFromFailOrQuit(); - return true; } public override bool OnExiting(ScreenExitEvent e) From d3a55d83c000dd1bf0174a9811585e214d605346 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Nov 2023 11:05:52 +0900 Subject: [PATCH 875/896] Schedule `FailScore` inside `onFail` instead of `onFailComplete` --- osu.Game/Screens/Play/Player.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b469ab443c..ff00b52f71 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -930,10 +930,22 @@ namespace osu.Game.Screens.Play failAnimationContainer.Start(); - if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) - Restart(true); + // Failures can be triggered either by a judgement, or by a mod. + // + // For the case of a judgement, due to ordering considerations, ScoreProcessor will not have received + // the final judgement which triggered the failure yet (see DrawableRuleset.NewResult handling above). + // + // A schedule here ensures that any lingering judgements from the current frame are applied before we + // finalise the score as "failed". + Schedule(() => + { + ScoreProcessor.FailScore(Score.ScoreInfo); + OnFail(); + + if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) + Restart(true); + }); - OnFail(); return true; } @@ -942,11 +954,6 @@ namespace osu.Game.Screens.Play /// private void onFailComplete() { - // fail completion is a good point to mark a score as failed, - // since the last judgement that caused the fail only applies to score processor after onFail. - // todo: this should probably be handled better. - ScoreProcessor.FailScore(Score.ScoreInfo); - GameplayClockContainer.Stop(); FailOverlay.Retries = RestartCount; From 7ceb49fbc095ae9b8bd91a6bf4c6c2d8763186f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 14:58:57 +0900 Subject: [PATCH 876/896] Add extra test coverage and handle case where still at loading screen --- .../Visual/Gameplay/TestSceneSpectator.cs | 64 +++++++++++++------ osu.Game/Screens/Play/SoloSpectatorScreen.cs | 33 +++++----- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index f5e4c5da51..1c7ede2b19 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(startTime: gameplay_start); @@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Gameplay return true; }); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); AddAssert("time is greater than seek target", () => currentFrameStableTime, () => Is.GreaterThan(gameplay_start)); @@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectatorScreen); start(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); @@ -155,7 +155,7 @@ namespace osu.Game.Tests.Visual.Gameplay loadSpectatingScreen(); start(); - waitForPlayer(); + waitForPlayerCurrent(); checkPaused(true); // send enough frames to ensure play won't be paused @@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(300); loadSpectatingScreen(); - waitForPlayer(); + waitForPlayerCurrent(); sendFrames(300); @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); Player lastPlayer = null; AddStep("store first player", () => lastPlayer = player); @@ -194,7 +194,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddAssert("player is different", () => lastPlayer != player); } @@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); - waitForPlayer(); + waitForPlayerCurrent(); checkPaused(true); sendFrames(); @@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); @@ -236,14 +236,14 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); // host starts playing a new session start(); - waitForPlayer(); + waitForPlayerCurrent(); } [Test] @@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } @@ -309,14 +309,14 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("send passed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Passed)); AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed); start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } @@ -327,7 +327,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddStep("send quit", () => spectatorClient.SendEndPlay(streamingUser.Id)); AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit); @@ -336,18 +336,19 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } [Test] - public void TestFailedState() + public void TestFailedStateDuringPlay() { loadSpectatingScreen(); start(); sendFrames(); - waitForPlayer(); + + waitForPlayerCurrent(); AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); @@ -356,7 +357,28 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); - waitForPlayer(); + waitForPlayerCurrent(); + AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); + } + + [Test] + public void TestFailedStateDuringLoading() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + + waitForPlayerLoader(); + + AddStep("send failed", () => spectatorClient.SendEndPlay(streamingUser.Id, SpectatedUserState.Failed)); + AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed); + + AddAssert("wait for player exit", () => Stack.CurrentScreen is SoloSpectatorScreen); + + start(); + sendFrames(); + waitForPlayerCurrent(); AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing); } @@ -368,7 +390,9 @@ namespace osu.Game.Tests.Visual.Gameplay private double currentFrameStableTime => player.ChildrenOfType().First().CurrentTime; - private void waitForPlayer() => AddUntilStep("wait for player", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + private void waitForPlayerLoader() => AddUntilStep("wait for loading", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); + + private void waitForPlayerCurrent() => AddUntilStep("wait for player current", () => this.ChildrenOfType().SingleOrDefault()?.IsCurrentScreen() == true); private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.SendStartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId)); diff --git a/osu.Game/Screens/Play/SoloSpectatorScreen.cs b/osu.Game/Screens/Play/SoloSpectatorScreen.cs index d4a9fc4b30..2db751402c 100644 --- a/osu.Game/Screens/Play/SoloSpectatorScreen.cs +++ b/osu.Game/Screens/Play/SoloSpectatorScreen.cs @@ -180,31 +180,32 @@ namespace osu.Game.Screens.Play protected override void FailGameplay(int userId) { - if (this.GetChildScreen() is SpectatorPlayer player) - player.AllowFail(); - - Schedule(() => + if (this.GetChildScreen() is SpectatorPlayerLoader loader) { - scheduledStart?.Cancel(); - immediateSpectatorGameplayState = null; - clearDisplay(); - }); + if (loader.GetChildScreen() is SpectatorPlayer player) + { + player.AllowFail(); + resetStartState(); + } + else + QuitGameplay(userId); + } } protected override void QuitGameplay(int userId) { // Importantly, don't schedule this call, as a child screen may be present (and will cause the schedule to not be run as expected). this.MakeCurrent(); - - Schedule(() => - { - scheduledStart?.Cancel(); - immediateSpectatorGameplayState = null; - - clearDisplay(); - }); + resetStartState(); } + private void resetStartState() => Schedule(() => + { + scheduledStart?.Cancel(); + immediateSpectatorGameplayState = null; + clearDisplay(); + }); + private void clearDisplay() { watchButton.Enabled.Value = false; From 289cda71b236d98f622f5c8920ba91dd14bb40b8 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 15:06:51 +0900 Subject: [PATCH 877/896] Fix inspections --- osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs | 8 ++++---- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 4bcd6b100a..459a8b0df5 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectWithFilter([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq"); @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelect([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); var firstImport = importScore(1); @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestFromSongSelectDifferentRuleset([Values] ScorePresentType type) { - AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke()); + AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo?.Invoke()); AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded); var firstImport = importScore(1); diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 8173375c29..78eb410a48 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Menu /// Assign the that this ButtonSystem should manage the position of. /// /// The instance of the logo to be assigned. If null, we are suspending from the screen that uses this ButtonSystem. - public void SetOsuLogo(OsuLogo logo) + public void SetOsuLogo(OsuLogo? logo) { this.logo = logo; From c126c46e2d25bcf2b1853ef80b869a230a9bb8ff Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 15:43:57 +0900 Subject: [PATCH 878/896] Remove legacy implementations (moved to osu-tools) --- .../Scoring/LegacyCatchHealthProcessor.cs | 237 ------------------ .../Scoring/LegacyOsuHealthProcessor.cs | 215 ---------------- 2 files changed, 452 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs delete mode 100644 osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs deleted file mode 100644 index ef13ba222c..0000000000 --- a/osu.Game.Rulesets.Catch/Scoring/LegacyCatchHealthProcessor.cs +++ /dev/null @@ -1,237 +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 System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Timing; -using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Catch.Scoring -{ - /// - /// Reference implementation for osu!stable's HP drain. - /// Cannot be used for gameplay. - /// - public partial class LegacyCatchHealthProcessor : DrainingHealthProcessor - { - private const double hp_bar_maximum = 200; - private const double hp_combo_geki = 14; - private const double hp_hit_300 = 6; - private const double hp_slider_tick = 3; - - public Action? OnIterationFail; - public Action? OnIterationSuccess; - public bool ApplyComboEndBonus { get; set; } = true; - - private double lowestHpEver; - private double lowestHpEnd; - private double lowestHpComboEnd; - private double hpRecoveryAvailable; - private double hpMultiplierNormal; - private double hpMultiplierComboEnd; - - public LegacyCatchHealthProcessor(double drainStartTime) - : base(drainStartTime) - { - } - - public override void ApplyBeatmap(IBeatmap beatmap) - { - lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60); - lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80); - lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80); - hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0); - - base.ApplyBeatmap(beatmap); - } - - protected override void ApplyResultInternal(JudgementResult result) - { - if (!IsSimulating) - throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); - } - - protected override void RevertResultInternal(JudgementResult result) - { - if (!IsSimulating) - throw new NotSupportedException("The legacy catch health processor is not supported for gameplay."); - } - - protected override void Reset(bool storeResults) - { - hpMultiplierNormal = 1; - hpMultiplierComboEnd = 1; - - base.Reset(storeResults); - } - - protected override double ComputeDrainRate() - { - double testDrop = 0.05; - double currentHp; - double currentHpUncapped; - - List<(HitObject hitObject, bool newCombo)> allObjects = enumerateHitObjects(Beatmap).Where(h => h.hitObject is Fruit || h.hitObject is Droplet || h.hitObject is Banana).ToList(); - - do - { - currentHp = hp_bar_maximum; - currentHpUncapped = hp_bar_maximum; - - double lowestHp = currentHp; - double lastTime = DrainStartTime; - int currentBreak = 0; - bool fail = false; - int comboTooLowCount = 0; - string failReason = string.Empty; - - for (int i = 0; i < allObjects.Count; i++) - { - HitObject h = allObjects[i].hitObject; - - // Find active break (between current and lastTime) - double localLastTime = lastTime; - double breakTime = 0; - - // Subtract any break time from the duration since the last object - if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) - { - BreakPeriod e = Beatmap.Breaks[currentBreak]; - - if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) - { - // consider break start equal to object end time for version 8+ since drain stops during this time - breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; - currentBreak++; - } - } - - reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); - - lastTime = h.GetEndTime(); - - if (currentHp < lowestHp) - lowestHp = currentHp; - - if (currentHp <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})"; - break; - } - - switch (h) - { - case Fruit: - if (ApplyComboEndBonus && (i == allObjects.Count - 1 || allObjects[i + 1].newCombo)) - { - increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); - - if (currentHp < lowestHpComboEnd) - { - if (++comboTooLowCount > 2) - { - hpMultiplierComboEnd *= 1.07; - hpMultiplierNormal *= 1.03; - fail = true; - failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})"; - } - } - } - else - increaseHp(hpMultiplierNormal * hp_hit_300); - - break; - - case Banana: - increaseHp(hpMultiplierNormal / 2); - break; - - case TinyDroplet: - increaseHp(hpMultiplierNormal * hp_slider_tick * 0.1); - break; - - case Droplet: - increaseHp(hpMultiplierNormal * hp_slider_tick); - break; - } - - if (fail) - break; - } - - if (!fail && currentHp < lowestHpEnd) - { - fail = true; - testDrop *= 0.94; - hpMultiplierComboEnd *= 1.01; - hpMultiplierNormal *= 1.01; - failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})"; - } - - double recovery = (currentHpUncapped - hp_bar_maximum) / allObjects.Count; - - if (!fail && recovery < hpRecoveryAvailable) - { - fail = true; - testDrop *= 0.96; - hpMultiplierComboEnd *= 1.02; - hpMultiplierNormal *= 1.01; - failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})"; - } - - if (fail) - { - OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); - continue; - } - - OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}"); - return testDrop / hp_bar_maximum; - } while (true); - - void reduceHp(double amount) - { - currentHpUncapped = Math.Max(0, currentHpUncapped - amount); - currentHp = Math.Max(0, currentHp - amount); - } - - void increaseHp(double amount) - { - currentHpUncapped += amount; - currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount)); - } - } - - private IEnumerable<(HitObject hitObject, bool newCombo)> enumerateHitObjects(IBeatmap beatmap) - { - return enumerateRecursively(beatmap.HitObjects); - - static IEnumerable<(HitObject hitObject, bool newCombo)> enumerateRecursively(IEnumerable hitObjects) - { - foreach (var hitObject in hitObjects) - { - // The combo end will either be attached to the hitobject itself if it has no children, or the very first child if it has children. - bool newCombo = (hitObject as IHasComboInformation)?.NewCombo ?? false; - - foreach ((HitObject nested, bool _) in enumerateRecursively(hitObject.NestedHitObjects)) - { - yield return (nested, newCombo); - - // Since the combo was attached to the first child, don't attach it to any other child or the parenting hitobject itself. - newCombo = false; - } - - yield return (hitObject, newCombo); - } - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs deleted file mode 100644 index e92c3c9b97..0000000000 --- a/osu.Game.Rulesets.Osu/Scoring/LegacyOsuHealthProcessor.cs +++ /dev/null @@ -1,215 +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.Linq; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Timing; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Osu.Scoring -{ - /// - /// Reference implementation for osu!stable's HP drain. - /// Cannot be used for gameplay. - /// - public partial class LegacyOsuHealthProcessor : DrainingHealthProcessor - { - private const double hp_bar_maximum = 200; - private const double hp_combo_geki = 14; - private const double hp_hit_300 = 6; - private const double hp_slider_repeat = 4; - private const double hp_slider_tick = 3; - - public Action? OnIterationFail; - public Action? OnIterationSuccess; - public bool ApplyComboEndBonus { get; set; } = true; - - private double lowestHpEver; - private double lowestHpEnd; - private double lowestHpComboEnd; - private double hpRecoveryAvailable; - private double hpMultiplierNormal; - private double hpMultiplierComboEnd; - - public LegacyOsuHealthProcessor(double drainStartTime) - : base(drainStartTime) - { - } - - public override void ApplyBeatmap(IBeatmap beatmap) - { - lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 195, 160, 60); - lowestHpComboEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 170, 80); - lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 198, 180, 80); - hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 8, 4, 0); - - base.ApplyBeatmap(beatmap); - } - - protected override void ApplyResultInternal(JudgementResult result) - { - if (!IsSimulating) - throw new NotSupportedException("The legacy osu! health processor is not supported for gameplay."); - } - - protected override void RevertResultInternal(JudgementResult result) - { - if (!IsSimulating) - throw new NotSupportedException("The legacy osu! health processor is not supported for gameplay."); - } - - protected override void Reset(bool storeResults) - { - hpMultiplierNormal = 1; - hpMultiplierComboEnd = 1; - - base.Reset(storeResults); - } - - protected override double ComputeDrainRate() - { - double testDrop = 0.05; - double currentHp; - double currentHpUncapped; - - do - { - currentHp = hp_bar_maximum; - currentHpUncapped = hp_bar_maximum; - - double lowestHp = currentHp; - double lastTime = DrainStartTime; - int currentBreak = 0; - bool fail = false; - int comboTooLowCount = 0; - string failReason = string.Empty; - - for (int i = 0; i < Beatmap.HitObjects.Count; i++) - { - HitObject h = Beatmap.HitObjects[i]; - - // Find active break (between current and lastTime) - double localLastTime = lastTime; - double breakTime = 0; - - // Subtract any break time from the duration since the last object - if (Beatmap.Breaks.Count > 0 && currentBreak < Beatmap.Breaks.Count) - { - BreakPeriod e = Beatmap.Breaks[currentBreak]; - - if (e.StartTime >= localLastTime && e.EndTime <= h.StartTime) - { - // consider break start equal to object end time for version 8+ since drain stops during this time - breakTime = (Beatmap.BeatmapInfo.BeatmapVersion < 8) ? (e.EndTime - e.StartTime) : e.EndTime - localLastTime; - currentBreak++; - } - } - - reduceHp(testDrop * (h.StartTime - lastTime - breakTime)); - - lastTime = h.GetEndTime(); - - if (currentHp < lowestHp) - lowestHp = currentHp; - - if (currentHp <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - failReason = $"hp too low ({currentHp / hp_bar_maximum} < {lowestHpEver / hp_bar_maximum})"; - break; - } - - double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); - double hpOverkill = Math.Max(0, hpReduction - currentHp); - reduceHp(hpReduction); - - if (h is Slider slider) - { - for (int j = 0; j < slider.RepeatCount + 2; j++) - increaseHp(hpMultiplierNormal * hp_slider_repeat); - foreach (var _ in slider.NestedHitObjects.OfType()) - increaseHp(hpMultiplierNormal * hp_slider_tick); - } - else if (h is Spinner spinner) - { - foreach (var _ in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) - increaseHp(hpMultiplierNormal * 1.7); - } - - if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - failReason = $"overkill ({currentHp / hp_bar_maximum} - {hpOverkill / hp_bar_maximum} <= {lowestHpEver / hp_bar_maximum})"; - break; - } - - if (ApplyComboEndBonus && (i == Beatmap.HitObjects.Count - 1 || ((OsuHitObject)Beatmap.HitObjects[i + 1]).NewCombo)) - { - increaseHp(hpMultiplierComboEnd * hp_combo_geki + hpMultiplierNormal * hp_hit_300); - - if (currentHp < lowestHpComboEnd) - { - if (++comboTooLowCount > 2) - { - hpMultiplierComboEnd *= 1.07; - hpMultiplierNormal *= 1.03; - fail = true; - failReason = $"combo end hp too low ({currentHp / hp_bar_maximum} < {lowestHpComboEnd / hp_bar_maximum})"; - break; - } - } - } - else - increaseHp(hpMultiplierNormal * hp_hit_300); - } - - if (!fail && currentHp < lowestHpEnd) - { - fail = true; - testDrop *= 0.94; - hpMultiplierComboEnd *= 1.01; - hpMultiplierNormal *= 1.01; - failReason = $"end hp too low ({currentHp / hp_bar_maximum} < {lowestHpEnd / hp_bar_maximum})"; - } - - double recovery = (currentHpUncapped - hp_bar_maximum) / Beatmap.HitObjects.Count; - - if (!fail && recovery < hpRecoveryAvailable) - { - fail = true; - testDrop *= 0.96; - hpMultiplierComboEnd *= 1.02; - hpMultiplierNormal *= 1.01; - failReason = $"recovery too low ({recovery / hp_bar_maximum} < {hpRecoveryAvailable / hp_bar_maximum})"; - } - - if (fail) - { - OnIterationFail?.Invoke($"FAILED drop {testDrop / hp_bar_maximum}: {failReason}"); - continue; - } - - OnIterationSuccess?.Invoke($"PASSED drop {testDrop / hp_bar_maximum}"); - return testDrop / hp_bar_maximum; - } while (true); - - void reduceHp(double amount) - { - currentHpUncapped = Math.Max(0, currentHpUncapped - amount); - currentHp = Math.Max(0, currentHp - amount); - } - - void increaseHp(double amount) - { - currentHpUncapped += amount; - currentHp = Math.Max(0, Math.Min(hp_bar_maximum, currentHp + amount)); - } - } - } -} From 36b45d34f7415327f80ed1a243a30af73390805f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 16:39:29 +0900 Subject: [PATCH 879/896] Check drag location on mouse down instead of drag start to avoid lenience issues --- osu.Game/Overlays/ChatOverlay.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 724f77ad71..54d5952bc3 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -251,10 +251,14 @@ namespace osu.Game.Overlays { } + protected override bool OnMouseDown(MouseDownEvent e) + { + isDraggingTopBar = topBar.DragBar.IsHovered; + return base.OnMouseDown(e); + } + protected override bool OnDragStart(DragStartEvent e) { - isDraggingTopBar = topBar.IsHovered; - if (!isDraggingTopBar) return base.OnDragStart(e); From 7600595e5dba09c0cd6b3a034be86b427dfc6a03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 16:39:54 +0900 Subject: [PATCH 880/896] Add drag bar on chat overlay to better signal resizability --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 80 +++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 0410174dc1..44cb07ca91 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -23,6 +22,8 @@ namespace osu.Game.Overlays.Chat private Color4 backgroundColour; + public Drawable DragBar = null!; + [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) { @@ -50,7 +51,7 @@ namespace osu.Game.Overlays.Chat Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = textures.Get("Icons/Hexacons/messaging"), - Size = new Vector2(18), + Size = new Vector2(24), }, // Placeholder text new OsuSpriteText @@ -64,19 +65,90 @@ namespace osu.Game.Overlays.Chat }, }, }, + DragBar = new DragArea + { + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colourProvider.Background4, + } }; } protected override bool OnHover(HoverEvent e) { - background.FadeColour(backgroundColour.Lighten(0.1f), 300, Easing.OutQuint); + DragBar.FadeIn(100); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - background.FadeColour(backgroundColour, 300, Easing.OutQuint); + DragBar.FadeOut(100); base.OnHoverLost(e); } + + private partial class DragArea : CompositeDrawable + { + private readonly Circle circle; + + public DragArea() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + circle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(150, 7), + Margin = new MarginPadding(12), + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + updateScale(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateScale(); + base.OnHoverLost(e); + } + + private bool dragging; + + protected override bool OnMouseDown(MouseDownEvent e) + { + dragging = true; + updateScale(); + return base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + dragging = false; + updateScale(); + base.OnMouseUp(e); + } + + private void updateScale() + { + if (dragging || IsHovered) + circle.FadeIn(100); + else + circle.FadeTo(0.6f, 100); + + if (dragging) + circle.ScaleTo(1f, 400, Easing.OutQuint); + else if (IsHovered) + circle.ScaleTo(1.05f, 400, Easing.OutElasticHalf); + else + circle.ScaleTo(1f, 500, Easing.OutQuint); + } + } } } From 59800821da1afe62e520af591b172eb34c80bee2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 16:44:18 +0900 Subject: [PATCH 881/896] 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 ea08992710..ddaa371014 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 53d5d6b010..c11dfd06f3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - + From 3b41480beffb8eda3ea8428f1d1a02dc81cfd4c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 17:46:02 +0900 Subject: [PATCH 882/896] Always show drag bar on mobile --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 44cb07ca91..807bb26502 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.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; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -67,7 +68,7 @@ namespace osu.Game.Overlays.Chat }, DragBar = new DragArea { - Alpha = 0, + Alpha = RuntimeInfo.IsMobile ? 1 : 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = colourProvider.Background4, @@ -77,13 +78,15 @@ namespace osu.Game.Overlays.Chat protected override bool OnHover(HoverEvent e) { - DragBar.FadeIn(100); + if (!RuntimeInfo.IsMobile) + DragBar.FadeIn(100); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - DragBar.FadeOut(100); + if (!RuntimeInfo.IsMobile) + DragBar.FadeOut(100); base.OnHoverLost(e); } From 290c3d63492315e700ff57d468585c0a8d3bc52a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 17:46:23 +0900 Subject: [PATCH 883/896] Clean up left-overs --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 807bb26502..330c991ce8 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -13,27 +13,22 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Resources.Localisation.Web; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays.Chat { public partial class ChatOverlayTopBar : Container { - private Box background = null!; - - private Color4 backgroundColour; - public Drawable DragBar = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) { - Children = new Drawable[] + Children = new[] { - background = new Box + new Box { RelativeSizeAxes = Axes.Both, - Colour = backgroundColour = colourProvider.Background3, + Colour = colourProvider.Background3, }, new GridContainer { From 95229cb33607ba8ddc43af8da387830f36f63d48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 17:19:03 +0900 Subject: [PATCH 884/896] Show gameplay when loading the skin editor from the main menu --- .../Overlays/SkinEditor/SkinEditorOverlay.cs | 65 +++++++++++++++++++ .../SkinEditor/SkinEditorSceneLibrary.cs | 32 +-------- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index d1e7b97efc..16fc6b6ec6 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -1,7 +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; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -9,12 +12,21 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; +using osu.Game.Screens.Select; +using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -31,12 +43,21 @@ namespace osu.Game.Overlays.SkinEditor private SkinEditor? skinEditor; + [Resolved] + private IPerformFromScreenRunner? performer { get; set; } + [Cached] public readonly EditorClipboard Clipboard = new EditorClipboard(); [Resolved] private OsuGame game { get; set; } = null!; + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private Bindable> mods { get; set; } = null!; + private OsuScreen? lastTargetScreen; private Vector2 lastDrawSize; @@ -72,6 +93,9 @@ namespace osu.Game.Overlays.SkinEditor { globallyDisableBeatmapSkinSetting(); + if (lastTargetScreen is MainMenu) + PresentGameplay(); + if (skinEditor != null) { skinEditor.Show(); @@ -105,6 +129,28 @@ namespace osu.Game.Overlays.SkinEditor globallyReenableBeatmapSkinSetting(); } + public void PresentGameplay() + { + performer?.PerformFromScreen(screen => + { + if (screen is Player) + return; + + var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); + + IReadOnlyList usableMods = mods.Value; + + if (replayGeneratingMod != null) + usableMods = usableMods.Append(replayGeneratingMod).ToArray(); + + if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid)) + mods.Value = mods.Value.Except(invalid).ToArray(); + + if (replayGeneratingMod != null) + screen.Push(new EndlessPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))); + }, new[] { typeof(Player), typeof(PlaySongSelect) }); + } + protected override void Update() { base.Update(); @@ -222,5 +268,24 @@ namespace osu.Game.Overlays.SkinEditor leasedBeatmapSkins?.Return(); leasedBeatmapSkins = null; } + + private partial class EndlessPlayer : ReplayPlayer + { + public EndlessPlayer(Func, Score> createScore) + : base(createScore, new PlayerConfiguration + { + ShowResults = false, + }) + { + } + + protected override void Update() + { + base.Update(); + + if (GameplayState.HasPassed) + GameplayClockContainer.Seek(0); + } + } } } diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs index 9b021632cf..7fa33ddcf5 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs @@ -1,10 +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 System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,12 +11,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Screens; -using osu.Game.Screens.Play; using osu.Game.Screens.Select; -using osu.Game.Utils; using osuTK; namespace osu.Game.Overlays.SkinEditor @@ -36,10 +29,7 @@ namespace osu.Game.Overlays.SkinEditor private IPerformFromScreenRunner? performer { get; set; } [Resolved] - private IBindable ruleset { get; set; } = null!; - - [Resolved] - private Bindable> mods { get; set; } = null!; + private SkinEditorOverlay skinEditorOverlay { get; set; } = null!; public SkinEditorSceneLibrary() { @@ -96,24 +86,7 @@ namespace osu.Game.Overlays.SkinEditor Text = SkinEditorStrings.Gameplay, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Action = () => performer?.PerformFromScreen(screen => - { - if (screen is Player) - return; - - var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod(); - - IReadOnlyList usableMods = mods.Value; - - if (replayGeneratingMod != null) - usableMods = usableMods.Append(replayGeneratingMod).ToArray(); - - if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid)) - mods.Value = mods.Value.Except(invalid).ToArray(); - - if (replayGeneratingMod != null) - screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods)))); - }, new[] { typeof(Player), typeof(PlaySongSelect) }) + Action = () => skinEditorOverlay.PresentGameplay(), }, } }, @@ -137,5 +110,6 @@ namespace osu.Game.Overlays.SkinEditor Content.CornerRadius = 5; } } + } } From 7153c823e8405713579f1a9d6c72f5ac5c2572b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 17:34:08 +0900 Subject: [PATCH 885/896] Choose a better beatmap if the intro is still playing Also skip intro time. --- osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs index 16fc6b6ec6..18d1c4c62b 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs @@ -52,12 +52,18 @@ namespace osu.Game.Overlays.SkinEditor [Resolved] private OsuGame game { get; set; } = null!; + [Resolved] + private MusicController music { get; set; } = null!; + [Resolved] private IBindable ruleset { get; set; } = null!; [Resolved] private Bindable> mods { get; set; } = null!; + [Resolved] + private IBindable beatmap { get; set; } = null!; + private OsuScreen? lastTargetScreen; private Vector2 lastDrawSize; @@ -133,6 +139,14 @@ namespace osu.Game.Overlays.SkinEditor { performer?.PerformFromScreen(screen => { + // If we're playing the intro, switch away to another beatmap. + if (beatmap.Value.BeatmapSetInfo.Protected) + { + music.NextTrack(); + Schedule(PresentGameplay); + return; + } + if (screen is Player) return; @@ -275,6 +289,7 @@ namespace osu.Game.Overlays.SkinEditor : base(createScore, new PlayerConfiguration { ShowResults = false, + AutomaticallySkipIntro = true, }) { } From 8314f656a3107b27fc82ddbad448de4c70bdf41d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 17:32:18 +0900 Subject: [PATCH 886/896] Encapsulate common HP logic from osu and catch HP calculations --- .../Scoring/CatchHealthProcessor.cs | 121 +------------- .../Scoring/OsuHealthProcessor.cs | 155 ++--------------- .../Scoring/LegacyDrainingHealthProcessor.cs | 158 ++++++++++++++++++ 3 files changed, 179 insertions(+), 255 deletions(-) create mode 100644 osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs index 6d831ad223..c3cc488941 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs @@ -1,138 +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 System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { - public partial class CatchHealthProcessor : DrainingHealthProcessor + public partial class CatchHealthProcessor : LegacyDrainingHealthProcessor { - public Action? OnIterationFail; - public Action? OnIterationSuccess; - - private double lowestHpEver; - private double lowestHpEnd; - private double hpRecoveryAvailable; - private double hpMultiplierNormal; - public CatchHealthProcessor(double drainStartTime) : base(drainStartTime) { } - public override void ApplyBeatmap(IBeatmap beatmap) - { - lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); - lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); - hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); + protected override IEnumerable EnumerateTopLevelHitObjects() => EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana); - base.ApplyBeatmap(beatmap); - } + protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty(); - protected override void Reset(bool storeResults) - { - hpMultiplierNormal = 1; - base.Reset(storeResults); - } - - protected override double ComputeDrainRate() - { - double testDrop = 0.00025; - double currentHp; - double currentHpUncapped; - - while (true) - { - currentHp = 1; - currentHpUncapped = 1; - - double lowestHp = currentHp; - double lastTime = DrainStartTime; - int currentBreak = 0; - bool fail = false; - - List allObjects = EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana).ToList(); - - for (int i = 0; i < allObjects.Count; i++) - { - HitObject h = allObjects[i]; - - while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) - { - // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. - // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, - // but this shouldn't have a noticeable impact in practice. - lastTime = h.StartTime; - currentBreak++; - } - - reduceHp(testDrop * (h.StartTime - lastTime)); - - lastTime = h.GetEndTime(); - - if (currentHp < lowestHp) - lowestHp = currentHp; - - if (currentHp <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); - break; - } - - increaseHp(h); - } - - if (!fail && currentHp < lowestHpEnd) - { - fail = true; - testDrop *= 0.94; - hpMultiplierNormal *= 1.01; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); - } - - double recovery = (currentHpUncapped - 1) / allObjects.Count; - - if (!fail && recovery < hpRecoveryAvailable) - { - fail = true; - testDrop *= 0.96; - hpMultiplierNormal *= 1.01; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); - } - - if (!fail) - { - OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); - return testDrop; - } - } - - void reduceHp(double amount) - { - currentHpUncapped = Math.Max(0, currentHpUncapped - amount); - currentHp = Math.Max(0, currentHp - amount); - } - - void increaseHp(HitObject hitObject) - { - double amount = healthIncreaseFor(hitObject.CreateJudgement().MaxResult); - currentHpUncapped += amount; - currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); - } - } - - protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.Type); - - private double healthIncreaseFor(HitResult result) + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) { double increase = 0; @@ -162,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Scoring break; } - return hpMultiplierNormal * increase; + return HpMultiplierNormal * increase; } } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs index 3c124b3162..7025a7be65 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHealthProcessor.cs @@ -1,166 +1,43 @@ // 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.Game.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Scoring { - public partial class OsuHealthProcessor : DrainingHealthProcessor + public partial class OsuHealthProcessor : LegacyDrainingHealthProcessor { - public Action? OnIterationFail; - public Action? OnIterationSuccess; - - private double lowestHpEver; - private double lowestHpEnd; - private double hpRecoveryAvailable; - private double hpMultiplierNormal; - public OsuHealthProcessor(double drainStartTime) : base(drainStartTime) { } - public override void ApplyBeatmap(IBeatmap beatmap) + protected override IEnumerable EnumerateTopLevelHitObjects() => Beatmap.HitObjects; + + protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) { - lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); - lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); - hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); - - base.ApplyBeatmap(beatmap); - } - - protected override void Reset(bool storeResults) - { - hpMultiplierNormal = 1; - base.Reset(storeResults); - } - - protected override double ComputeDrainRate() - { - double testDrop = 0.00025; - double currentHp; - double currentHpUncapped; - - while (true) + switch (hitObject) { - currentHp = 1; - currentHpUncapped = 1; + case Slider slider: + foreach (var nested in slider.NestedHitObjects) + yield return nested; - double lowestHp = currentHp; - double lastTime = DrainStartTime; - int currentBreak = 0; - bool fail = false; + break; - for (int i = 0; i < Beatmap.HitObjects.Count; i++) - { - HitObject h = Beatmap.HitObjects[i]; + case Spinner spinner: + foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) + yield return nested; - while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) - { - // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. - // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, - // but this shouldn't have a noticeable impact in practice. - lastTime = h.StartTime; - currentBreak++; - } - - reduceHp(testDrop * (h.StartTime - lastTime)); - - lastTime = h.GetEndTime(); - - if (currentHp < lowestHp) - lowestHp = currentHp; - - if (currentHp <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); - break; - } - - double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); - double hpOverkill = Math.Max(0, hpReduction - currentHp); - reduceHp(hpReduction); - - switch (h) - { - case Slider slider: - { - foreach (var nested in slider.NestedHitObjects) - increaseHp(nested); - break; - } - - case Spinner spinner: - { - foreach (var nested in spinner.NestedHitObjects.Where(t => t is not SpinnerBonusTick)) - increaseHp(nested); - break; - } - } - - // Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners - // will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version. - if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) - { - fail = true; - testDrop *= 0.96; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"); - break; - } - - increaseHp(h); - } - - if (!fail && currentHp < lowestHpEnd) - { - fail = true; - testDrop *= 0.94; - hpMultiplierNormal *= 1.01; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); - } - - double recovery = (currentHpUncapped - 1) / Beatmap.HitObjects.Count; - - if (!fail && recovery < hpRecoveryAvailable) - { - fail = true; - testDrop *= 0.96; - hpMultiplierNormal *= 1.01; - OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); - } - - if (!fail) - { - OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); - return testDrop; - } - } - - void reduceHp(double amount) - { - currentHpUncapped = Math.Max(0, currentHpUncapped - amount); - currentHp = Math.Max(0, currentHp - amount); - } - - void increaseHp(HitObject hitObject) - { - double amount = healthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult); - currentHpUncapped += amount; - currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); + break; } } - protected override double GetHealthIncreaseFor(JudgementResult result) => healthIncreaseFor(result.HitObject, result.Type); - - private double healthIncreaseFor(HitObject hitObject, HitResult result) + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) { double increase = 0; @@ -206,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break; } - return hpMultiplierNormal * increase; + return HpMultiplierNormal * increase; } } } diff --git a/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs new file mode 100644 index 0000000000..ce2f7d5624 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/LegacyDrainingHealthProcessor.cs @@ -0,0 +1,158 @@ +// 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.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + /// + /// A that matches legacy drain rate calculations as best as possible. + /// + public abstract partial class LegacyDrainingHealthProcessor : DrainingHealthProcessor + { + public Action? OnIterationFail; + public Action? OnIterationSuccess; + + protected double HpMultiplierNormal { get; private set; } + + private double lowestHpEver; + private double lowestHpEnd; + private double hpRecoveryAvailable; + + protected LegacyDrainingHealthProcessor(double drainStartTime) + : base(drainStartTime) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + lowestHpEver = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.975, 0.8, 0.3); + lowestHpEnd = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.99, 0.9, 0.4); + hpRecoveryAvailable = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.04, 0.02, 0); + + base.ApplyBeatmap(beatmap); + } + + protected override void Reset(bool storeResults) + { + HpMultiplierNormal = 1; + base.Reset(storeResults); + } + + protected override double ComputeDrainRate() + { + double testDrop = 0.00025; + double currentHp; + double currentHpUncapped; + + while (true) + { + currentHp = 1; + currentHpUncapped = 1; + + double lowestHp = currentHp; + double lastTime = DrainStartTime; + int currentBreak = 0; + bool fail = false; + int topLevelObjectCount = 0; + + foreach (var h in EnumerateTopLevelHitObjects()) + { + topLevelObjectCount++; + + while (currentBreak < Beatmap.Breaks.Count && Beatmap.Breaks[currentBreak].EndTime <= h.StartTime) + { + // If two hitobjects are separated by a break period, there is no drain for the full duration between the hitobjects. + // This differs from legacy (version < 8) beatmaps which continue draining until the break section is entered, + // but this shouldn't have a noticeable impact in practice. + lastTime = h.StartTime; + currentBreak++; + } + + reduceHp(testDrop * (h.StartTime - lastTime)); + + lastTime = h.GetEndTime(); + + if (currentHp < lowestHp) + lowestHp = currentHp; + + if (currentHp <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: hp too low ({currentHp} < {lowestHpEver})"); + break; + } + + double hpReduction = testDrop * (h.GetEndTime() - h.StartTime); + double hpOverkill = Math.Max(0, hpReduction - currentHp); + reduceHp(hpReduction); + + foreach (var nested in EnumerateNestedHitObjects(h)) + increaseHp(nested); + + // Note: Because HP is capped during the above increases, long sliders (with many ticks) or spinners + // will appear to overkill at lower drain levels than they should. However, it is also not correct to simply use the uncapped version. + if (hpOverkill > 0 && currentHp - hpOverkill <= lowestHpEver) + { + fail = true; + testDrop *= 0.96; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: overkill ({currentHp} - {hpOverkill} <= {lowestHpEver})"); + break; + } + + increaseHp(h); + } + + if (!fail && currentHp < lowestHpEnd) + { + fail = true; + testDrop *= 0.94; + HpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: end hp too low ({currentHp} < {lowestHpEnd})"); + } + + double recovery = (currentHpUncapped - 1) / Math.Max(1, topLevelObjectCount); + + if (!fail && recovery < hpRecoveryAvailable) + { + fail = true; + testDrop *= 0.96; + HpMultiplierNormal *= 1.01; + OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); + } + + if (!fail) + { + OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); + return testDrop; + } + } + + void reduceHp(double amount) + { + currentHpUncapped = Math.Max(0, currentHpUncapped - amount); + currentHp = Math.Max(0, currentHp - amount); + } + + void increaseHp(HitObject hitObject) + { + double amount = GetHealthIncreaseFor(hitObject, hitObject.CreateJudgement().MaxResult); + currentHpUncapped += amount; + currentHp = Math.Max(0, Math.Min(1, currentHp + amount)); + } + } + + protected sealed override double GetHealthIncreaseFor(JudgementResult result) => GetHealthIncreaseFor(result.HitObject, result.Type); + + protected abstract IEnumerable EnumerateTopLevelHitObjects(); + + protected abstract IEnumerable EnumerateNestedHitObjects(HitObject hitObject); + + protected abstract double GetHealthIncreaseFor(HitObject hitObject, HitResult result); + } +} From a44edfdeddadaac693c63312fb6d3cc14593ee0e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 19:37:57 +0900 Subject: [PATCH 887/896] Fix incorrect sample for top level edit button --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 78eb410a48..259efad8b3 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -138,7 +138,7 @@ namespace osu.Game.Screens.Menu buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); - buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-default-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); + buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); if (host.CanExit) From 901561533600690974d0c89756cc850dca5ec7d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Nov 2023 19:42:30 +0900 Subject: [PATCH 888/896] Update test to work with new drag bar location --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 55e6b54af7..8a2a66f60f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -180,11 +180,8 @@ namespace osu.Game.Tests.Visual.Online }); AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default); - AddStep("Click top bar", () => - { - InputManager.MoveMouseTo(chatOverlayTopBar); - InputManager.PressButton(MouseButton.Left); - }); + AddStep("Move mouse to drag bar", () => InputManager.MoveMouseTo(chatOverlayTopBar.DragBar)); + AddStep("Click drag bar", () => InputManager.PressButton(MouseButton.Left)); AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300))); AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("Store new height", () => newHeight = chatOverlay.Height); From a6cf1e5d2eabb3cd5f32da0b213e20c1ab601a91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Nov 2023 00:53:25 +0900 Subject: [PATCH 889/896] Make osu! logo do something when in edit submenu --- osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 259efad8b3..ca5bef985e 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -311,6 +311,10 @@ namespace osu.Game.Screens.Menu case ButtonSystemState.Play: buttonsPlay.First().TriggerClick(); return false; + + case ButtonSystemState.Edit: + buttonsEdit.First().TriggerClick(); + return false; } } From 6f66819e5152de82d807522089465b79a82b3850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 10:44:50 +0900 Subject: [PATCH 890/896] Privatise setter --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 1cea198300..84fd342493 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Chat { public partial class ChatOverlayTopBar : Container { - public Drawable DragBar = null!; + public Drawable DragBar { get; private set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) From 7f788058cdadf4f8196b4095afdf2c265b81edab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 11:35:10 +0900 Subject: [PATCH 891/896] Revert incorrect xmldoc change --- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index e7435adf29..47e2dd807a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Spectator public IBindableList PlayingUsers => playingUsers; /// - /// Whether the spectated user is playing. + /// Whether the local user is playing. /// private bool isPlaying { get; set; } From 3f48f4acdff18744317665661eb0119154eceff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 12:06:08 +0900 Subject: [PATCH 892/896] Remove blank line --- osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs index 7fa33ddcf5..a682285549 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs @@ -110,6 +110,5 @@ namespace osu.Game.Overlays.SkinEditor Content.CornerRadius = 5; } } - } } From 7e3bb5f8dba1a3357a6a6729a557d1e0b13714c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 12:09:13 +0900 Subject: [PATCH 893/896] Make skin editor overlay dependency nullable to fix tests --- osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs index a682285549..5a283c0e8d 100644 --- a/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs +++ b/osu.Game/Overlays/SkinEditor/SkinEditorSceneLibrary.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.SkinEditor private IPerformFromScreenRunner? performer { get; set; } [Resolved] - private SkinEditorOverlay skinEditorOverlay { get; set; } = null!; + private SkinEditorOverlay? skinEditorOverlay { get; set; } public SkinEditorSceneLibrary() { @@ -86,7 +86,7 @@ namespace osu.Game.Overlays.SkinEditor Text = SkinEditorStrings.Gameplay, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Action = () => skinEditorOverlay.PresentGameplay(), + Action = () => skinEditorOverlay?.PresentGameplay(), }, } }, From a4be28a2aebf4bf14c72a1aee35d60fad654c88e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Nov 2023 17:53:06 +0900 Subject: [PATCH 894/896] Don't show buttons on fail overlay when player interaction is disabled --- osu.Game/Screens/Play/FailOverlay.cs | 15 +++++++++++++-- osu.Game/Screens/Play/Player.cs | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/FailOverlay.cs b/osu.Game/Screens/Play/FailOverlay.cs index abfc401998..210ae5ceb6 100644 --- a/osu.Game/Screens/Play/FailOverlay.cs +++ b/osu.Game/Screens/Play/FailOverlay.cs @@ -26,11 +26,22 @@ namespace osu.Game.Screens.Play public override LocalisableString Header => GameplayMenuOverlayStrings.FailedHeader; + private readonly bool showButtons; + + public FailOverlay(bool showButtons = true) + { + this.showButtons = showButtons; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { - AddButton(GameplayMenuOverlayStrings.Retry, colours.YellowDark, () => OnRetry?.Invoke()); - AddButton(GameplayMenuOverlayStrings.Quit, new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + if (showButtons) + { + AddButton(GameplayMenuOverlayStrings.Retry, colours.YellowDark, () => OnRetry?.Invoke()); + AddButton(GameplayMenuOverlayStrings.Quit, new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + } + // from #10339 maybe this is a better visual effect Add(new Container { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ff00b52f71..1c97efcff7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -267,7 +267,7 @@ namespace osu.Game.Screens.Play createGameplayComponents(Beatmap.Value) } }, - FailOverlay = new FailOverlay + FailOverlay = new FailOverlay(Configuration.AllowUserInteraction) { SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false), OnRetry = () => Restart(), From d9242278105febc065dcc57b4bd318a89dcb190a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 24 Nov 2023 18:04:57 +0900 Subject: [PATCH 895/896] Add `ManiaHealthProcessor` that uses the legacy drain rate algorithm --- .../Scoring/ManiaHealthProcessor.cs | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs index e63a037ca9..183550eb7b 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaHealthProcessor.cs @@ -1,23 +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.Game.Rulesets.Judgements; +using System.Collections.Generic; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { - public partial class ManiaHealthProcessor : DrainingHealthProcessor + public partial class ManiaHealthProcessor : LegacyDrainingHealthProcessor { - /// public ManiaHealthProcessor(double drainStartTime) - : base(drainStartTime, 1.0) + : base(drainStartTime) { } - protected override HitResult GetSimulatedHitResult(Judgement judgement) + protected override IEnumerable EnumerateTopLevelHitObjects() => Beatmap.HitObjects; + + protected override IEnumerable EnumerateNestedHitObjects(HitObject hitObject) => hitObject.NestedHitObjects; + + protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result) { - // Users are not expected to attain perfect judgements for all notes due to the tighter hit window. - return judgement.MaxResult == HitResult.Perfect ? HitResult.Great : judgement.MaxResult; + double increase = 0; + + switch (result) + { + case HitResult.Miss: + switch (hitObject) + { + case HeadNote: + case TailNote: + return -(Beatmap.Difficulty.DrainRate + 1) * 0.00375; + + default: + return -(Beatmap.Difficulty.DrainRate + 1) * 0.0075; + } + + case HitResult.Meh: + return -(Beatmap.Difficulty.DrainRate + 1) * 0.0016; + + case HitResult.Ok: + return 0; + + case HitResult.Good: + increase = 0.004 - Beatmap.Difficulty.DrainRate * 0.0004; + break; + + case HitResult.Great: + increase = 0.005 - Beatmap.Difficulty.DrainRate * 0.0005; + break; + + case HitResult.Perfect: + increase = 0.0055 - Beatmap.Difficulty.DrainRate * 0.0005; + break; + } + + return HpMultiplierNormal * increase; } } } From 3f73610ee765b5543e851627efdd64ecba93eccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Nov 2023 15:06:11 +0900 Subject: [PATCH 896/896] Update framework ^& resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 4 ++-- osu.iOS.props | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index ea08992710..3b90b1675c 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 53d5d6b010..7e5c5be4ea 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -23,6 +23,6 @@ iossimulator-x64 - +