From 442347df8eee74c903a4564ab8ec92a8b20c7964 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Fri, 19 Feb 2021 18:04:25 +1100 Subject: [PATCH 01/46] Fix clockrate adjusted difficulty calculations bug in strain decay When starting a new section, the starting strain value was calculated using the unadjusted timing value, meaning decay curves were essentially being stretched or squashed according to the clockrate. This caused incorrect strain peaks for any section where the peak occurs at the start of the section (none of the objects in the section added enough strain after decay to exceed the starting strain). This bug caused star ratings with clockrates above 1 to be lower than they should and below 1 to be higher than they should. --- osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs | 4 ++-- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 2 +- .../Difficulty/Preprocessing/DifficultyHitObject.cs | 6 ++++++ osu.Game/Rulesets/Difficulty/Skills/Skill.cs | 6 +++--- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 7ebc1ff752..56fb138b1f 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -71,8 +71,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills } protected override double GetPeakStrain(double offset) - => applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base) - + applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base); + => applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base) + + applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base); private double applyDecay(double value, double deltaTime, double decayBase) => value * Math.Pow(decayBase, deltaTime / 1000); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index f15e5e1df0..8c2292dcaa 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Difficulty foreach (Skill s in skills) { s.SaveCurrentPeak(); - s.StartNewSectionFrom(currentSectionEnd); + s.StartNewSectionFrom(currentSectionEnd / clockRate); } currentSectionEnd += sectionLength; diff --git a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs index ebbffb5143..fa578d55f0 100644 --- a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs +++ b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs @@ -25,6 +25,11 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing /// public readonly double DeltaTime; + /// + /// Start time of . + /// + public readonly double StartTime; + /// /// Creates a new . /// @@ -36,6 +41,7 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing BaseObject = hitObject; LastObject = lastObject; DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate; + StartTime = hitObject.StartTime / clockRate; } } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 1063a24b27..44ce78c8e3 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// /// Sets the initial strain level for a new section. /// - /// The beginning of the new section in milliseconds. + /// The beginning of the new section in milliseconds, adjusted by clockrate. public void StartNewSectionFrom(double time) { // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries. @@ -87,9 +87,9 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// /// Retrieves the peak strain at a point in time. /// - /// The time to retrieve the peak strain at. + /// The time to retrieve the peak strain at, adjusted by clockrate. /// The peak strain. - protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].BaseObject.StartTime); + protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime); /// /// Returns the calculated difficulty value representing all processed s. From 417bb07b366394a41bef13dcc2a1c2a79891bfbd Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sat, 20 Feb 2021 16:52:19 +1100 Subject: [PATCH 02/46] Update tests with fixed diffcalc values --- .../CatchDifficultyCalculatorTest.cs | 2 +- .../ManiaDifficultyCalculatorTest.cs | 2 +- osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs | 4 ++-- .../TaikoDifficultyCalculatorTest.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index f4ee3f5a42..5580358f89 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(5.0565038923984691d, "diffcalc-test")] + [TestCase(5.169743871843191d, "diffcalc-test")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new CatchModDoubleTime()); diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index 09ca04be8a..6e6500a339 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(2.7646128945056723d, "diffcalc-test")] + [TestCase(2.7879104989252959d, "diffcalc-test")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new ManiaModDoubleTime()); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index a365ea10d4..b6db989231 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.6228371119393064d, "diffcalc-test")] - [TestCase(1.2864585434597433d, "zero-length-sliders")] + [TestCase(8.7212283220504574d, "diffcalc-test")] + [TestCase(1.3212137310562277d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index eb21c02d5f..dd3c6b317a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -19,8 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(3.1473940254109078d, "diffcalc-test")] - [TestCase(3.1473940254109078d, "diffcalc-test-strong")] + [TestCase(3.1704781712282624d, "diffcalc-test")] + [TestCase(3.1704781712282624d, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new TaikoModDoubleTime()); From 013ddc734c09c110b9590b71cd1f04aa1a5bf021 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Mar 2021 17:04:35 +0900 Subject: [PATCH 03/46] Fix osu!catch fruit showing on plate when hidden mod is enabled Closes https://github.com/ppy/osu/issues/12065. --- osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs | 13 ++++++++++++- osu.Game.Rulesets.Catch/UI/Catcher.cs | 5 +++-- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 7 ++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index 4b008d2734..5c5e41234d 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -3,13 +3,16 @@ using System.Linq; using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModHidden : ModHidden + public class CatchModHidden : ModHidden, IApplicableToDrawableRuleset { public override string Description => @"Play with fading fruits."; public override double ScoreMultiplier => 1.06; @@ -17,6 +20,14 @@ namespace osu.Game.Rulesets.Catch.Mods private const double fade_out_offset_multiplier = 0.6; private const double fade_out_duration_multiplier = 0.44; + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset; + var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield; + + catchPlayfield.CatcherArea.CatchFruitOnPlate = false; + } + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { base.ApplyNormalVisibilityState(hitObject, state); diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index ed875e7002..42be745c2e 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition <= catcherPosition + halfCatchWidth; } - public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result) + public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result, bool placeOnPlate) { var catchResult = (CatchJudgementResult)result; catchResult.CatcherAnimationState = CurrentState; @@ -237,7 +237,8 @@ namespace osu.Game.Rulesets.Catch.UI { var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2); - placeCaughtObject(palpableObject, positionInStack); + if (placeOnPlate) + placeCaughtObject(palpableObject, positionInStack); if (hitLighting.Value) addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 44adbd5512..29c95ec61c 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Catch.UI public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; + /// + /// Whether fruit should appear on the plate. + /// + public bool CatchFruitOnPlate { get; set; } = true; + public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); @@ -41,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) { - MovableCatcher.OnNewResult(hitObject, result); + MovableCatcher.OnNewResult(hitObject, result, CatchFruitOnPlate); if (!result.Type.IsScorable()) return; From 6808719efabe9cd5997e51f916ecd9f56535b1f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Mar 2021 17:05:28 +0900 Subject: [PATCH 04/46] Update test scene --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 48efd73222..ddb6194899 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.Catch.Tests Add(drawableObject); drawableObject.OnLoadComplete += _ => { - catcher.OnNewResult(drawableObject, result); + catcher.OnNewResult(drawableObject, result, true); drawableObject.Expire(); }; } From 1c1af981443df917fe27ace265c41c9f6a515760 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 26 Mar 2021 11:47:41 +0900 Subject: [PATCH 05/46] Update values --- osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index ab9b7f4847..afd94f4570 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.7212283220504574d, "diffcalc-test")] - [TestCase(1.3212137310562277d, "zero-length-sliders")] + [TestCase(8.7212283220412345d, "diffcalc-test")] + [TestCase(1.3212137158641493d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); From 010db8968fa664cb7d0a44ec0da727c3a9776773 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sat, 27 Mar 2021 18:38:23 +1100 Subject: [PATCH 06/46] Adjust wording of xmldoc --- .../Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs index fa578d55f0..1bf0d8a222 100644 --- a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs +++ b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing public readonly double DeltaTime; /// - /// Start time of . + /// Clockrate adjusted start time of . /// public readonly double StartTime; From 068f00d8a035834b7e21bec15318b6aafd48f9fc Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sat, 27 Mar 2021 18:38:43 +1100 Subject: [PATCH 07/46] Add EndTime to DifficultyHitObject for future convenience --- .../Difficulty/Preprocessing/DifficultyHitObject.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs index 1bf0d8a222..ece8219071 100644 --- a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs +++ b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs @@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing /// public readonly double StartTime; + /// + /// Clockrate adjusted start time of . + /// + public readonly double EndTime; + /// /// Creates a new . /// @@ -42,6 +47,7 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing LastObject = lastObject; DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate; StartTime = hitObject.StartTime / clockRate; + EndTime = hitObject.GetEndTime() / clockRate; } } } From ecb66ad2e2213e52f3460964be7a2c8f21c69a08 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 29 Mar 2021 15:33:54 +0900 Subject: [PATCH 08/46] Fix up xmldoc --- .../Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs index ece8219071..576fbb2af0 100644 --- a/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs +++ b/osu.Game/Rulesets/Difficulty/Preprocessing/DifficultyHitObject.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing public readonly double StartTime; /// - /// Clockrate adjusted start time of . + /// Clockrate adjusted end time of . /// public readonly double EndTime; From 8a0fcf20ede3a63a074072899b33cb02cd83fdec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 12:32:42 +0900 Subject: [PATCH 09/46] Move offset settings up for more logical ordering --- .../Settings/Sections/Input/TabletSettings.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index bd0f7ddc4c..571d79baf8 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -110,12 +110,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input } }, new SettingsSlider - { - TransferValueOnCommit = true, - LabelText = "Aspect Ratio", - Current = aspectRatio - }, - new SettingsSlider { TransferValueOnCommit = true, LabelText = "X Offset", @@ -127,6 +121,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Y Offset", Current = offsetY }, + new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Aspect Ratio", + Current = aspectRatio + }, new SettingsCheckbox { LabelText = "Lock aspect ratio", From 8dfff999f9ced7f2e550df44fcf3c76e871055fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 12:40:50 +0900 Subject: [PATCH 10/46] Add rotation slider --- .../Visual/Settings/TestSceneTabletSettings.cs | 2 ++ .../Settings/Sections/Input/TabletAreaSelection.cs | 9 +++++++++ .../Overlays/Settings/Sections/Input/TabletSettings.cs | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index a7f6c8c0d3..a62980addf 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -45,6 +45,8 @@ namespace osu.Game.Tests.Visual.Settings public Bindable AreaOffset { get; } = new Bindable(); public Bindable AreaSize { get; } = new Bindable(); + public Bindable Rotation { get; } = new Bindable(); + public IBindable Tablet => tablet; private readonly Bindable tablet = new Bindable(); diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs index ecb8acce54..f61742093c 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs @@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly Bindable areaOffset = new Bindable(); private readonly Bindable areaSize = new Bindable(); + private readonly BindableNumber rotation = new BindableNumber(); + private readonly IBindable tablet = new Bindable(); private OsuSpriteText tabletName; @@ -124,6 +126,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}"; }, true); + rotation.BindTo(handler.Rotation); + rotation.BindValueChanged(val => + { + usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint) + .OnComplete(_ => checkBounds()); // required as we are using SSDQ. + }); + tablet.BindTo(handler.Tablet); tablet.BindValueChanged(_ => Scheduler.AddOnce(updateTabletDetails)); diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 571d79baf8..9d128f8db3 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableNumber sizeX = new BindableNumber { MinValue = 10 }; private readonly BindableNumber sizeY = new BindableNumber { MinValue = 10 }; + private readonly BindableNumber rotation = new BindableNumber { MinValue = 0, MaxValue = 360 }; + [Resolved] private GameHost host { get; set; } @@ -122,6 +124,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = offsetY }, new SettingsSlider + { + TransferValueOnCommit = true, + LabelText = "Rotation", + Current = rotation + }, + new SettingsSlider { TransferValueOnCommit = true, LabelText = "Aspect Ratio", @@ -153,6 +161,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { base.LoadComplete(); + rotation.BindTo(tabletHandler.Rotation); + areaOffset.BindTo(tabletHandler.AreaOffset); areaOffset.BindValueChanged(val => { From 1dfd08eded521c40576cc746216f3b487e239e9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 13:01:48 +0900 Subject: [PATCH 11/46] Add tablet rotation configuration --- .../Sections/Input/RotationPresetButtons.cs | 109 ++++++++++++++++++ .../Settings/Sections/Input/TabletSettings.cs | 1 + 2 files changed, 110 insertions(+) create mode 100644 osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs diff --git a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs new file mode 100644 index 0000000000..3e8da9f7d0 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs @@ -0,0 +1,109 @@ +// 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.Framework.Graphics.Containers; +using osu.Framework.Input.Handlers.Tablet; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + internal class RotationPresetButtons : FillFlowContainer + { + private readonly ITabletHandler tabletHandler; + + private Bindable rotation; + + private const int height = 50; + + public RotationPresetButtons(ITabletHandler tabletHandler) + { + this.tabletHandler = tabletHandler; + + RelativeSizeAxes = Axes.X; + Height = height; + + for (int i = 0; i < 360; i += 90) + { + var presetRotation = i; + + Add(new RotationButton(i) + { + RelativeSizeAxes = Axes.X, + Height = height, + Width = 0.25f, + Text = $"{presetRotation}ยบ", + Action = () => tabletHandler.Rotation.Value = presetRotation, + }); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + rotation = tabletHandler.Rotation.GetBoundCopy(); + rotation.BindValueChanged(val => + { + foreach (var b in Children.OfType()) + b.IsSelected = b.Preset == val.NewValue; + }, true); + } + + public class RotationButton : TriangleButton + { + [Resolved] + private OsuColour colours { get; set; } + + public readonly int Preset; + + public RotationButton(int preset) + { + Preset = preset; + } + + private bool isSelected; + + public bool IsSelected + { + get => isSelected; + set + { + if (value == isSelected) + return; + + isSelected = value; + + if (IsLoaded) + updateColour(); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateColour(); + } + + private void updateColour() + { + if (isSelected) + { + BackgroundColour = colours.BlueDark; + Triangles.ColourDark = colours.BlueDarker; + Triangles.ColourLight = colours.Blue; + } + else + { + BackgroundColour = colours.Gray4; + Triangles.ColourDark = colours.Gray5; + Triangles.ColourLight = colours.Gray6; + } + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 9d128f8db3..d770c18878 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -129,6 +129,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Rotation", Current = rotation }, + new RotationPresetButtons(tabletHandler), new SettingsSlider { TransferValueOnCommit = true, From 89bea2868a9ca68a8c09a9c93db6f504e9022a96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 14:33:55 +0900 Subject: [PATCH 12/46] Move bool one level down --- osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 9 +++++++-- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 7 +------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index 5c5e41234d..bba42dea97 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Mods var drawableCatchRuleset = (DrawableCatchRuleset)drawableRuleset; var catchPlayfield = (CatchPlayfield)drawableCatchRuleset.Playfield; - catchPlayfield.CatcherArea.CatchFruitOnPlate = false; + catchPlayfield.CatcherArea.MovableCatcher.CatchFruitOnPlate = false; } protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 42be745c2e..5d57e84b75 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public bool HyperDashing => hyperDashModifier != 1; + /// + /// Whether fruit should appear on the plate. + /// + public bool CatchFruitOnPlate { get; set; } = true; + /// /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. /// @@ -223,7 +228,7 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition <= catcherPosition + halfCatchWidth; } - public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result, bool placeOnPlate) + public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result) { var catchResult = (CatchJudgementResult)result; catchResult.CatcherAnimationState = CurrentState; @@ -237,7 +242,7 @@ namespace osu.Game.Rulesets.Catch.UI { var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2); - if (placeOnPlate) + if (CatchFruitOnPlate) placeCaughtObject(palpableObject, positionInStack); if (hitLighting.Value) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 29c95ec61c..44adbd5512 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -21,11 +21,6 @@ namespace osu.Game.Rulesets.Catch.UI public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; - /// - /// Whether fruit should appear on the plate. - /// - public bool CatchFruitOnPlate { get; set; } = true; - public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); @@ -46,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) { - MovableCatcher.OnNewResult(hitObject, result, CatchFruitOnPlate); + MovableCatcher.OnNewResult(hitObject, result); if (!result.Type.IsScorable()) return; From f12353a99e5b38c57d1578460b542b27d07a1cec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 14:57:06 +0900 Subject: [PATCH 13/46] Update forgotten test scene usage --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index ddb6194899..48efd73222 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -242,7 +242,7 @@ namespace osu.Game.Rulesets.Catch.Tests Add(drawableObject); drawableObject.OnLoadComplete += _ => { - catcher.OnNewResult(drawableObject, result, true); + catcher.OnNewResult(drawableObject, result); drawableObject.Expire(); }; } From 90c75a64cf9e30e213821706e8d54de262d2b629 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 15:24:11 +0900 Subject: [PATCH 14/46] Fix legacy control point precision having an adverse effect on the editor --- osu.Game/Screens/Edit/Timing/DifficultySection.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs index 9d80ca0b14..97d110c502 100644 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs @@ -28,7 +28,16 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable; + var selectedPointBindable = point.NewValue.SpeedMultiplierBindable; + + // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint). + // generally that level of precision could only be set by externally editing the .osu file, so at the point + // a user is looking to update this within the editor it should be safe to obliterate this additional precision. + double expectedPrecision = new DifficultyControlPoint().SpeedMultiplierBindable.Precision; + if (selectedPointBindable.Precision < expectedPrecision) + selectedPointBindable.Precision = expectedPrecision; + + multiplierSlider.Current = selectedPointBindable; multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } From 1d968009c2c329a1abd107409c9e37c302ae36c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 16:07:04 +0900 Subject: [PATCH 15/46] Add osu!mania key filtering using "keys=4" at song select --- .../Beatmaps/ManiaBeatmapConverter.cs | 8 ++++- .../ManiaFilterCriteria.cs | 33 +++++++++++++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 6 ++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 7a0e3b2b76..756207a201 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps if (IsForCurrentRuleset) { - TargetColumns = (int)Math.Max(1, roundedCircleSize); + TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo); if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS) { @@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps originalTargetColumns = TargetColumns; } + internal static int GetColumnCountForNonConvert(BeatmapInfo beatmap) + { + var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); + return (int)Math.Max(1, roundedCircleSize); + } + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs new file mode 100644 index 0000000000..c9a1ae84d4 --- /dev/null +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Filter; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Rulesets.Mania +{ + public class ManiaFilterCriteria : IRulesetFilterCriteria + { + private FilterCriteria.OptionalRange keys; + + public bool Matches(BeatmapInfo beatmap) + { + return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID) && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap)); + } + + public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) + { + switch (key) + { + case "key": + case "keys": + return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value); + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index d624e094ad..88b63606b9 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -22,6 +22,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Filter; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; @@ -382,6 +383,11 @@ namespace osu.Game.Rulesets.Mania } } }; + + public override IRulesetFilterCriteria CreateRulesetFilterCriteria() + { + return new ManiaFilterCriteria(); + } } public enum PlayfieldType From e769ef45be36d1bf700969bf76bcab5c0dac2f99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 16:55:39 +0900 Subject: [PATCH 16/46] Fix misplaced parenthesis --- osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index c9a1ae84d4..d9a278ef29 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania public bool Matches(BeatmapInfo beatmap) { - return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID) && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap)); + return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap))); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) From 56428a027e53e9e89dcb321a5f58eb32c899d52d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 16:56:19 +0900 Subject: [PATCH 17/46] Change static method to public --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 756207a201..26393c8edb 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps originalTargetColumns = TargetColumns; } - internal static int GetColumnCountForNonConvert(BeatmapInfo beatmap) + public static int GetColumnCountForNonConvert(BeatmapInfo beatmap) { var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); return (int)Math.Max(1, roundedCircleSize); From 633e6130bf434c628d7be98eeba0f0fff9d30cf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 19:45:22 +0900 Subject: [PATCH 18/46] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 3682a44b9f..5b65670869 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6d571218fc..6a7f7e7026 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ceb46eae87..4aa3ad1c61 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From fb0079fb9f641c7a7c769a3144684cb90c0320a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 30 Mar 2021 22:42:31 +0900 Subject: [PATCH 19/46] Fix accuracy displaying incorrectly in online contexts Closes #12221. --- osu.Game/Users/UserStatistics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 04a358436e..5ddcd86d28 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -45,7 +45,7 @@ namespace osu.Game.Users public double Accuracy; [JsonIgnore] - public string DisplayAccuracy => Accuracy.FormatAccuracy(); + public string DisplayAccuracy => (Accuracy / 100).FormatAccuracy(); [JsonProperty(@"play_count")] public int PlayCount; From ded91b32a4facc2e55d909428999401ffec0ac80 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 31 Mar 2021 12:11:43 +0900 Subject: [PATCH 20/46] Add failing test --- .../TestSceneHoldNoteInput.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 7ae69bf7d7..2357778948 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -288,6 +288,42 @@ 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 = + { + BaseDifficulty = 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); + AddAssert("one tick missed", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Count(j => j.Type == HitResult.LargeTickMiss) == 1); + assertTailJudgement(HitResult.Ok); + } + private void assertHeadJudgement(HitResult result) => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); From f78d628878a0244c1de1b0efa26d9e206d593771 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 31 Mar 2021 12:21:07 +0900 Subject: [PATCH 21/46] Improve assertions --- .../TestSceneHoldNoteInput.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 2357778948..42ea12214f 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -320,21 +320,24 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); assertHeadJudgement(HitResult.Perfect); - AddAssert("one tick missed", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Count(j => j.Type == HitResult.LargeTickMiss) == 1); + assertLastTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Ok); } private void assertHeadJudgement(HitResult result) - => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); + => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result); private void assertTailJudgement(HitResult result) - => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result); + => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type == result); private void assertNoteJudgement(HitResult result) - => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result); + => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type == result); private void assertTickJudgement(HitResult result) - => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick + => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Any(j => j.Type == result)); + + private void assertLastTickJudgement(HitResult result) + => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type == result); private ScoreAccessibleReplayPlayer currentPlayer; From 43e48406caa2253ccdc580c53ad22d0b39ad7ebf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 31 Mar 2021 12:21:14 +0900 Subject: [PATCH 22/46] Miss all ticks when hold note is hit --- .../Objects/Drawables/DrawableHoldNote.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 4f062753a6..828ee7b03e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,6 +233,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { + foreach (var tick in tickContainer) + { + if (!tick.Judged) + tick.MissForcefully(); + } + ApplyResult(r => r.Type = r.Judgement.MaxResult); endHold(); } From e0c61f4dc556189de3e841242568df93269aafdd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 13:57:57 +0900 Subject: [PATCH 23/46] Fix retry count not updating correctly Regressed with changes to player reference retention logic. Could add a test but the logic is so local now it seems quite redundant. --- osu.Game/Screens/Play/PlayerLoader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 7d906cdc5b..2bbc4a0469 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -309,10 +309,8 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; - var restartCount = player?.RestartCount + 1 ?? 0; - player = createPlayer(); - player.RestartCount = restartCount; + player.RestartCount = ++restartCount; player.RestartRequested = restartRequested; LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false); @@ -428,6 +426,8 @@ namespace osu.Game.Screens.Play private Bindable muteWarningShownOnce; + private int restartCount; + private void showMuteWarningIfNeeded() { if (!muteWarningShownOnce.Value) From 0c53b4eb938a2770c5b2420dcf646cce0bd16c23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 14:09:38 +0900 Subject: [PATCH 24/46] Fix wrong counting and add test --- .../Navigation/TestSceneScreenNavigation.cs | 23 +++++++++++++++++++ osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index f2bb518b2e..3e25e22b5f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -43,6 +43,29 @@ namespace osu.Game.Tests.Visual.Navigation exitViaEscapeAndConfirm(); } + [Test] + public void TestRetryCountIncrements() + { + Player player = null; + + PushAndConfirm(() => new TestSongSelect()); + + AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddAssert("retry count is 0", () => player.RestartCount == 0); + + AddStep("attempt to retry", () => player.ChildrenOfType().First().Action()); + AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player); + + AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddAssert("retry count is 1", () => player.RestartCount == 1); + } + [Test] public void TestRetryFromResults() { diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 2bbc4a0469..679b3c7313 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -310,7 +310,7 @@ namespace osu.Game.Screens.Play return; player = createPlayer(); - player.RestartCount = ++restartCount; + player.RestartCount = restartCount++; player.RestartRequested = restartRequested; LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false); From 30cae46cbdf44021e850f63122d90d803052ca9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 31 Mar 2021 14:57:28 +0900 Subject: [PATCH 25/46] Group large drag drop imports into a single operation --- osu.Desktop/OsuGameDesktop.cs | 34 +++++++++++++++++++++++++++++++--- osu.Game/OsuGameBase.cs | 3 +++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index b2487568ce..0c21c75290 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -18,6 +19,7 @@ using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; using osu.Desktop.Windows; +using osu.Framework.Threading; using osu.Game.IO; namespace osu.Desktop @@ -144,13 +146,39 @@ namespace osu.Desktop desktopWindow.DragDrop += f => fileDrop(new[] { f }); } + private readonly List importableFiles = new List(); + private ScheduledDelegate importSchedule; + private void fileDrop(string[] filePaths) { - var firstExtension = Path.GetExtension(filePaths.First()); + lock (importableFiles) + { + var firstExtension = Path.GetExtension(filePaths.First()); - if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; + if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; - Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); + importableFiles.AddRange(filePaths); + + Logger.Log($"Adding {filePaths.Length} files for import"); + + // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms. + // In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch. + importSchedule?.Cancel(); + importSchedule = Scheduler.AddDelayed(handlePendingImports, 100); + } + } + + private void handlePendingImports() + { + lock (importableFiles) + { + Logger.Log($"Handling batch import of {importableFiles.Count} files"); + + var paths = importableFiles.ToArray(); + importableFiles.Clear(); + + Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); + } } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e1c7b67a8c..7eef0f7158 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -429,6 +429,9 @@ namespace osu.Game public async Task Import(params string[] paths) { + if (paths.Length == 0) + return; + var extension = Path.GetExtension(paths.First())?.ToLowerInvariant(); foreach (var importer in fileImporters) From af478fb2eb6052ddfcc0b99137d9605d2ff06681 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Apr 2021 22:02:32 +0900 Subject: [PATCH 26/46] Add abstract spectator screen class --- osu.Game/Screens/Spectate/GameplayState.cs | 37 +++ osu.Game/Screens/Spectate/SpectatorScreen.cs | 237 +++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 osu.Game/Screens/Spectate/GameplayState.cs create mode 100644 osu.Game/Screens/Spectate/SpectatorScreen.cs diff --git a/osu.Game/Screens/Spectate/GameplayState.cs b/osu.Game/Screens/Spectate/GameplayState.cs new file mode 100644 index 0000000000..4579b9c07c --- /dev/null +++ b/osu.Game/Screens/Spectate/GameplayState.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Spectate +{ + /// + /// The gameplay state of a spectated user. This class is immutable. + /// + public class GameplayState + { + /// + /// The score which the user is playing. + /// + public readonly Score Score; + + /// + /// The ruleset which the user is playing. + /// + public readonly Ruleset Ruleset; + + /// + /// The beatmap which the user is playing. + /// + public readonly WorkingBeatmap Beatmap; + + public GameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) + { + Score = score; + Ruleset = ruleset; + Beatmap = beatmap; + } + } +} diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs new file mode 100644 index 0000000000..a534581807 --- /dev/null +++ b/osu.Game/Screens/Spectate/SpectatorScreen.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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.Spectator; +using osu.Game.Replays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Scoring; +using osu.Game.Users; + +namespace osu.Game.Screens.Spectate +{ + /// + /// A which spectates one or more users. + /// + public abstract class SpectatorScreen : OsuScreen + { + private readonly int[] userIds; + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private SpectatorStreamingClient spectatorClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + private readonly object stateLock = new object(); + + private readonly Dictionary userMap = new Dictionary(); + private readonly Dictionary spectatorStates = new Dictionary(); + private readonly Dictionary gameplayStates = new Dictionary(); + + private IBindable> managerUpdated; + + /// + /// Creates a new . + /// + /// The users to spectate. + protected SpectatorScreen(params int[] userIds) + { + this.userIds = userIds; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + spectatorClient.OnUserBeganPlaying += userBeganPlaying; + spectatorClient.OnUserFinishedPlaying += userFinishedPlaying; + spectatorClient.OnNewFrames += userSentFrames; + + foreach (var id in userIds) + { + userLookupCache.GetUserAsync(id).ContinueWith(u => Schedule(() => + { + if (u.Result == null) + return; + + lock (stateLock) + userMap[id] = u.Result; + + spectatorClient.WatchUser(id); + }), TaskContinuationOptions.OnlyOnRanToCompletion); + } + + managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); + } + + private void beatmapUpdated(ValueChangedEvent> beatmap) + { + if (!beatmap.NewValue.TryGetTarget(out var beatmapSet)) + return; + + lock (stateLock) + { + foreach (var (userId, state) in spectatorStates) + { + if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) + updateGameplayState(userId); + } + } + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (state.RulesetID == null || state.BeatmapID == null) + return; + + lock (stateLock) + { + if (!userMap.ContainsKey(userId)) + return; + + spectatorStates[userId] = state; + OnUserStateChanged(userId, state); + + updateGameplayState(userId); + } + } + + private void updateGameplayState(int userId) + { + lock (stateLock) + { + Debug.Assert(userMap.ContainsKey(userId)); + + var spectatorState = spectatorStates[userId]; + var user = userMap[userId]; + + var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance(); + if (resolvedRuleset == null) + return; + + var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == spectatorState.BeatmapID); + if (resolvedBeatmap == null) + return; + + var score = new Score + { + ScoreInfo = new ScoreInfo + { + Beatmap = resolvedBeatmap, + User = user, + Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), + Ruleset = resolvedRuleset.RulesetInfo, + }, + Replay = new Replay { HasReceivedAllFrames = false }, + }; + + var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); + + gameplayStates[userId] = gameplayState; + StartGameplay(userId, gameplayState); + } + } + + private void userSentFrames(int userId, FrameDataBundle bundle) + { + lock (stateLock) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.TryGetValue(userId, out var gameplayState)) + return; + + // The ruleset instance should be guaranteed to be in sync with the score via ScoreLock. + Debug.Assert(gameplayState.Ruleset != null && gameplayState.Ruleset.RulesetInfo.Equals(gameplayState.Score.ScoreInfo.Ruleset)); + + foreach (var frame in bundle.Frames) + { + IConvertibleReplayFrame convertibleFrame = gameplayState.Ruleset.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, gameplayState.Beatmap.Beatmap); + + var convertedFrame = (ReplayFrame)convertibleFrame; + convertedFrame.Time = frame.Time; + + gameplayState.Score.Replay.Frames.Add(convertedFrame); + } + } + } + + private void userFinishedPlaying(int userId, SpectatorState state) + { + lock (stateLock) + { + if (!userMap.ContainsKey(userId)) + return; + + if (!gameplayStates.TryGetValue(userId, out var gameplayState)) + return; + + gameplayState.Score.Replay.HasReceivedAllFrames = true; + + gameplayStates.Remove(userId); + EndGameplay(userId); + } + } + + /// + /// Invoked when a spectated user's state has changed. + /// + /// The user whose state has changed. + /// The new state. + protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState); + + /// + /// Starts gameplay for a user. + /// + /// The user to start gameplay for. + /// The gameplay state. + protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState); + + /// + /// Ends gameplay for a user. + /// + /// The user to end gameplay for. + protected abstract void EndGameplay(int userId); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorClient != null) + { + spectatorClient.OnUserBeganPlaying -= userBeganPlaying; + spectatorClient.OnUserFinishedPlaying -= userFinishedPlaying; + spectatorClient.OnNewFrames -= userSentFrames; + + lock (stateLock) + { + foreach (var (userId, _) in userMap) + spectatorClient.StopWatchingUser(userId); + } + } + + managerUpdated?.UnbindAll(); + } + } +} From 9e95441aa63ef92065204aa349cbd7abe2f44f62 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Apr 2021 22:08:52 +0900 Subject: [PATCH 27/46] Rename Spectator -> SoloSpectator --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 8 ++++---- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- osu.Game/Screens/Play/{Spectator.cs => SoloSpectator.cs} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Screens/Play/{Spectator.cs => SoloSpectator.cs} (99%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 4a0e1282c4..b59f6a4eb7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay // used just to show beatmap card for the time being. protected override bool UseOnlineAPI => true; - private Spectator spectatorScreen; + private SoloSpectator spectatorScreen; [Resolved] private OsuGameBase game { get; set; } @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddAssert("screen hasn't changed", () => Stack.CurrentScreen is Spectator); + AddAssert("screen hasn't changed", () => Stack.CurrentScreen is SoloSpectator); start(); sendFrames(); @@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(-1234); sendFrames(); - AddAssert("screen didn't change", () => Stack.CurrentScreen is Spectator); + AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); } private OsuFramedReplayInputHandler replayHandler => @@ -226,7 +226,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void loadSpectatingScreen() { - AddStep("load screen", () => LoadScreen(spectatorScreen = new Spectator(testSpectatorStreamingClient.StreamingUser))); + AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(testSpectatorStreamingClient.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 c89699f2ee..336430fd9b 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -137,7 +137,7 @@ namespace osu.Game.Overlays.Dashboard Text = "Watch", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(User))), + Action = () => game?.PerformFromScreen(s => s.Push(new SoloSpectator(User))), Enabled = { Value = User.Id != api.LocalUser.Value.Id } } } diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs similarity index 99% rename from osu.Game/Screens/Play/Spectator.cs rename to osu.Game/Screens/Play/SoloSpectator.cs index 28311f5113..9be42b52b3 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -37,7 +37,7 @@ using osuTK; namespace osu.Game.Screens.Play { [Cached(typeof(IPreviewTrackOwner))] - public class Spectator : OsuScreen, IPreviewTrackOwner + public class SoloSpectator : OsuScreen, IPreviewTrackOwner { private readonly User targetUser; @@ -88,7 +88,7 @@ namespace osu.Game.Screens.Play /// private bool newStatePending; - public Spectator([NotNull] User targetUser) + public SoloSpectator([NotNull] User targetUser) { this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); } From 9bc2a486e0c40baf2db334f6ef4cf1fdc5977a0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Apr 2021 22:09:51 +0900 Subject: [PATCH 28/46] Make SoloSpectator use the new SpectatorScreen class --- osu.Game/Screens/Play/SoloSpectator.cs | 233 ++++++------------------- 1 file changed, 53 insertions(+), 180 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 9be42b52b3..f8ed7b585f 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.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. -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.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -24,73 +20,54 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.Settings; -using osu.Game.Replays; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Replays.Types; -using osu.Game.Scoring; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.Spectate; using osu.Game.Users; using osuTK; namespace osu.Game.Screens.Play { [Cached(typeof(IPreviewTrackOwner))] - public class SoloSpectator : OsuScreen, IPreviewTrackOwner + public class SoloSpectator : SpectatorScreen, IPreviewTrackOwner { + [NotNull] private readonly User targetUser; - [Resolved] - private Bindable beatmap { get; set; } - - [Resolved] - private Bindable ruleset { get; set; } - - private Ruleset rulesetInstance; - - [Resolved] - private Bindable> mods { get; set; } - [Resolved] private IAPIProvider api { get; set; } [Resolved] - private SpectatorStreamingClient spectatorStreaming { get; set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } + private PreviewTrackManager previewTrackManager { get; set; } [Resolved] private RulesetStore rulesets { get; set; } [Resolved] - private PreviewTrackManager previewTrackManager { get; set; } - - private Score score; - - private readonly object scoreLock = new object(); + private BeatmapManager beatmaps { get; set; } private Container beatmapPanelContainer; - - private SpectatorState state; - - private IBindable> managerUpdated; - private TriangleButton watchButton; - private SettingsCheckbox automaticDownload; - private BeatmapSetInfo onlineBeatmap; /// - /// Becomes true if a new state is waiting to be loaded (while this screen was not active). + /// The player's immediate online gameplay state. + /// This doesn't reflect the gameplay state being watched by the user if is non-null. /// - private bool newStatePending; + private GameplayState immediateGameplayState; + + /// + /// The gameplay state that is pending to be watched, upon this screen becoming current. + /// + private GameplayState pendingGameplayState; + + private GetBeatmapSetRequest onlineBeatmapRequest; public SoloSpectator([NotNull] User targetUser) + : base(targetUser.Id) { - this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); + this.targetUser = targetUser; } [BackgroundDependencyLoader] @@ -173,7 +150,7 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = attemptStart, + Action = () => attemptStart(immediateGameplayState), Enabled = { Value = false } } } @@ -185,169 +162,81 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); - - spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; - spectatorStreaming.OnUserFinishedPlaying += userFinishedPlaying; - spectatorStreaming.OnNewFrames += userSentFrames; - - spectatorStreaming.WatchUser(targetUser.Id); - - managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); - managerUpdated.BindValueChanged(beatmapUpdated); - automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - private void beatmapUpdated(ValueChangedEvent> beatmap) + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) => Schedule(() => { - if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) - Schedule(attemptStart); - } + clearDisplay(); + showBeatmapPanel(spectatorState); + }); - private void userSentFrames(int userId, FrameDataBundle data) + protected override void StartGameplay(int userId, GameplayState gameplayState) => Schedule(() => { - // this is not scheduled as it handles propagation of frames even when in a child screen (at which point we are not alive). - // probably not the safest way to handle this. - - if (userId != targetUser.Id) - return; - - lock (scoreLock) - { - // this should never happen as the server sends the user's state on watching, - // but is here as a safety measure. - if (score == null) - return; - - // rulesetInstance should be guaranteed to be in sync with the score via scoreLock. - Debug.Assert(rulesetInstance != null && rulesetInstance.RulesetInfo.Equals(score.ScoreInfo.Ruleset)); - - foreach (var frame in data.Frames) - { - IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap); - - var convertedFrame = (ReplayFrame)convertibleFrame; - convertedFrame.Time = frame.Time; - - score.Replay.Frames.Add(convertedFrame); - } - } - } - - private void userBeganPlaying(int userId, SpectatorState state) - { - if (userId != targetUser.Id) - return; - - this.state = state; + pendingGameplayState = null; + immediateGameplayState = gameplayState; if (this.IsCurrentScreen()) - Schedule(attemptStart); + attemptStart(gameplayState); else - newStatePending = true; - } + pendingGameplayState = gameplayState; + + watchButton.Enabled.Value = true; + }); + + protected override void EndGameplay(int userId) => Schedule(() => + { + pendingGameplayState = null; + immediateGameplayState = null; + + Schedule(clearDisplay); + + watchButton.Enabled.Value = false; + }); public override void OnResuming(IScreen last) { base.OnResuming(last); - if (newStatePending) + if (pendingGameplayState != null) { - attemptStart(); - newStatePending = false; + attemptStart(pendingGameplayState); + pendingGameplayState = null; } } - private void userFinishedPlaying(int userId, SpectatorState state) - { - if (userId != targetUser.Id) - return; - - lock (scoreLock) - { - if (score != null) - { - score.Replay.HasReceivedAllFrames = true; - score = null; - } - } - - Schedule(clearDisplay); - } - private void clearDisplay() { watchButton.Enabled.Value = false; + onlineBeatmapRequest?.Cancel(); beatmapPanelContainer.Clear(); previewTrackManager.StopAnyPlaying(this); } - private void attemptStart() + private void attemptStart(GameplayState gameplayState) { - clearDisplay(); - showBeatmapPanel(state); - - var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); - - // ruleset not available - if (resolvedRuleset == null) + if (gameplayState == null) return; - if (state.BeatmapID == null) - return; + Beatmap.Value = gameplayState.Beatmap; + Ruleset.Value = gameplayState.Ruleset.RulesetInfo; - var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); - - if (resolvedBeatmap == null) - { - return; - } - - lock (scoreLock) - { - score = new Score - { - ScoreInfo = new ScoreInfo - { - Beatmap = resolvedBeatmap, - User = targetUser, - Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), - Ruleset = resolvedRuleset.RulesetInfo, - }, - Replay = new Replay { HasReceivedAllFrames = false }, - }; - - ruleset.Value = resolvedRuleset.RulesetInfo; - rulesetInstance = resolvedRuleset; - - beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); - watchButton.Enabled.Value = true; - - this.Push(new SpectatorPlayerLoader(score)); - } + this.Push(new SpectatorPlayerLoader(gameplayState.Score)); } private void showBeatmapPanel(SpectatorState state) { - if (state?.BeatmapID == null) - { - onlineBeatmap = null; - return; - } + Debug.Assert(state.BeatmapID != null); - var req = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); - req.Success += res => Schedule(() => + onlineBeatmapRequest = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); + onlineBeatmapRequest.Success += res => Schedule(() => { - if (state != this.state) - return; - onlineBeatmap = res.ToBeatmapSet(rulesets); beatmapPanelContainer.Child = new GridBeatmapPanel(onlineBeatmap); checkForAutomaticDownload(); }); - api.Queue(req); + api.Queue(onlineBeatmapRequest); } private void checkForAutomaticDownload() @@ -369,21 +258,5 @@ namespace osu.Game.Screens.Play previewTrackManager.StopAnyPlaying(this); return base.OnExiting(next); } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (spectatorStreaming != null) - { - spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; - spectatorStreaming.OnUserFinishedPlaying -= userFinishedPlaying; - spectatorStreaming.OnNewFrames -= userSentFrames; - - spectatorStreaming.StopWatchingUser(targetUser.Id); - } - - managerUpdated?.UnbindAll(); - } } } From c3c7c18549e281503d3225db9bc0d3fbc85c1511 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Apr 2021 23:48:26 +0900 Subject: [PATCH 29/46] Fix tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index b59f6a4eb7..9d85a9995d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -11,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Online; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; @@ -29,6 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + // used just to show beatmap card for the time being. protected override bool UseOnlineAPI => true; @@ -301,5 +307,14 @@ namespace osu.Game.Tests.Visual.Gameplay }); } } + + internal class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User + { + Id = lookup, + Username = $"User {lookup}" + }); + } } } From 6a286c5e2128c3103bd36548434f29260aec806d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:16:02 +0000 Subject: [PATCH 30/46] Bump Microsoft.NET.Test.Sdk from 16.9.1 to 16.9.4 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.9.1 to 16.9.4. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.9.1...v16.9.4) Signed-off-by: dependabot-preview[bot] --- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 728af5124e..c2d9a923d9 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index af16f39563..64e934efd2 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 3d2d1f3fec..f743d65db3 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index fa00922706..eab144592f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index e36b3cdc74..0e1f6f6b0c 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index b20583dd7e..a4e52f8cd4 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -5,7 +5,7 @@ - + From fcd56dba44a84e3e752e97f0eb3ed6409407f5ac Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Apr 2021 01:38:10 +0300 Subject: [PATCH 31/46] Guard against same ruleset file with loaded assembly filenames instead --- osu.Game/Rulesets/RulesetStore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index deabea57ef..eb5271aa17 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -173,7 +174,7 @@ namespace osu.Game.Rulesets { var filename = Path.GetFileNameWithoutExtension(file); - if (loadedAssemblies.Values.Any(t => t.Namespace == filename)) + if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) return; try From 5b1dc7d2b426e505da4b67cf8876404078a7f72f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 2 Apr 2021 02:45:26 +0300 Subject: [PATCH 32/46] Remove unused using directive --- osu.Game/Rulesets/RulesetStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index eb5271aa17..4261ee3d47 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; From e1aa9278272eabe7f6a975f3c78f4c03fca9122c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Apr 2021 13:20:15 +0900 Subject: [PATCH 33/46] Add dropdown option to export score --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 5608002513..da1bbd18c7 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -61,6 +61,9 @@ namespace osu.Game.Online.Leaderboards [Resolved(CanBeNull = true)] private SongSelect songSelect { get; set; } + [Resolved] + private ScoreManager scoreManager { get; set; } + public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true) { this.score = score; @@ -388,6 +391,9 @@ namespace osu.Game.Online.Leaderboards if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods)); + if (score.Files.Count > 0) + items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score))); + if (score.ID != 0) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); From 6d4d574a659c5148a7400003c6896990b2b689aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Apr 2021 14:10:25 +0900 Subject: [PATCH 34/46] Fix exported replay filenames not having full metadata --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index a898e10e4f..bf7906bd5c 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -147,7 +147,7 @@ namespace osu.Game.Beatmaps { string version = string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]"; - return $"{Metadata} {version}".Trim(); + return $"{Metadata ?? BeatmapSet?.Metadata} {version}".Trim(); } public bool Equals(BeatmapInfo other) From 5063cd957f8013c060aff0d5bc14b6a4ec58a9e1 Mon Sep 17 00:00:00 2001 From: Amber Date: Fri, 2 Apr 2021 02:54:35 -0500 Subject: [PATCH 35/46] Force hit sample to play when Classic mod is enabled --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 4 ++++ osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/DrawableSliderTail.cs | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 5470d0fcb4..21fde7cdc7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -79,6 +79,10 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableSliderHead head: head.TrackFollowCircle = !NoSliderHeadMovement.Value; break; + + case DrawableSliderTail tail: + tail.AlwaysPlaySample = true; + break; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 9122f347d0..4288f00e3a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -280,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { // rather than doing it this way, we should probably attach the sample to the tail circle. // this can only be done after we stop using LegacyLastTick. - if (TailCircle.IsHit) + if (TailCircle.IsHit || TailCircle.AlwaysPlaySample) base.PlaySamples(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 6a8e02e886..ad6b98cba8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public override bool DisplayResult => false; + /// + /// Whether the hit sample should always be played, regardless of whether the tail was actually hit. + /// + public bool AlwaysPlaySample { get; set; } + public bool Tracking { get; set; } private SkinnableDrawable circlePiece; @@ -44,6 +49,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { + AlwaysPlaySample = false; Origin = Anchor.Centre; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); From 45d16fb9164014de9990b6342f2a2d04b3805ba8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Apr 2021 16:56:47 +0900 Subject: [PATCH 36/46] Rename event parameter for clarity --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index a534581807..25dd482112 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -83,9 +83,9 @@ namespace osu.Game.Screens.Spectate managerUpdated.BindValueChanged(beatmapUpdated); } - private void beatmapUpdated(ValueChangedEvent> beatmap) + private void beatmapUpdated(ValueChangedEvent> e) { - if (!beatmap.NewValue.TryGetTarget(out var beatmapSet)) + if (!e.NewValue.TryGetTarget(out var beatmapSet)) return; lock (stateLock) From 48e9985782cb5537b1053d6beb05342409a7a921 Mon Sep 17 00:00:00 2001 From: Amber Date: Fri, 2 Apr 2021 03:10:28 -0500 Subject: [PATCH 37/46] Make "AlwaysPlayTailSample" a mod setting rather than a hardcoded constant. --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 21fde7cdc7..ec74225774 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")] public Bindable FixedFollowCircleHitArea { get; } = new BindableBool(true); + [SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")] + public Bindable AlwaysPlayTailSample { get; } = new BindableBool(true); + public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) @@ -81,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSliderTail tail: - tail.AlwaysPlaySample = true; + tail.AlwaysPlaySample = AlwaysPlayTailSample.Value; break; } } From 5ac36a24625eb80c5f897ed7929788e08bb988c4 Mon Sep 17 00:00:00 2001 From: Amber Date: Fri, 2 Apr 2021 03:56:23 -0500 Subject: [PATCH 38/46] Switch AlwaysPlaySample to SamplePlaysOnlyOnHit in DrawableSliderTail for conformity --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index ec74225774..882f848190 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSliderTail tail: - tail.AlwaysPlaySample = AlwaysPlayTailSample.Value; + tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value; break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 4288f00e3a..04708a5ece 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -280,7 +280,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { // rather than doing it this way, we should probably attach the sample to the tail circle. // this can only be done after we stop using LegacyLastTick. - if (TailCircle.IsHit || TailCircle.AlwaysPlaySample) + if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit) base.PlaySamples(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index ad6b98cba8..4b9c10ab67 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Whether the hit sample should always be played, regardless of whether the tail was actually hit. /// - public bool AlwaysPlaySample { get; set; } + public bool SamplePlaysOnlyOnHit { get; set; } = true; public bool Tracking { get; set; } @@ -49,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { - AlwaysPlaySample = false; Origin = Anchor.Centre; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); From 9c3d15171c6ba033970550e6e158a3fce750c3f6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Apr 2021 18:00:28 +0900 Subject: [PATCH 39/46] Reword xmldoc slightly --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 4b9c10ab67..87f098dd29 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; /// - /// Whether the hit sample should always be played, regardless of whether the tail was actually hit. + /// Whether the hit samples only play on successful hits. + /// If false, the hit samples will also play on misses. /// public bool SamplePlaysOnlyOnHit { get; set; } = true; From d2950105fb391fffe4463248ca3d172286b54d09 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Apr 2021 20:31:34 +0900 Subject: [PATCH 40/46] Add comment explaining use of lock --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 25dd482112..4d285d519e 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Spectate [Resolved] private UserLookupCache userLookupCache { get; set; } + // A lock is used to synchronise access to spectator/gameplay states, since this class is a screen which may become non-current and stop receiving updates at any point. private readonly object stateLock = new object(); private readonly Dictionary userMap = new Dictionary(); From cd53074941d864e0e1eaa33d8008f1730e8339a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Apr 2021 21:27:20 +0900 Subject: [PATCH 41/46] Schedule spectator callbacks --- osu.Game/Screens/Play/SoloSpectator.cs | 69 +++++++++----------- osu.Game/Screens/Spectate/SpectatorScreen.cs | 6 +- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index f8ed7b585f..820d776e63 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -53,15 +54,10 @@ namespace osu.Game.Screens.Play /// /// The player's immediate online gameplay state. - /// This doesn't reflect the gameplay state being watched by the user if is non-null. + /// This doesn't always reflect the gameplay state being watched. /// private GameplayState immediateGameplayState; - /// - /// The gameplay state that is pending to be watched, upon this screen becoming current. - /// - private GameplayState pendingGameplayState; - private GetBeatmapSetRequest onlineBeatmapRequest; public SoloSpectator([NotNull] User targetUser) @@ -150,7 +146,7 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = () => attemptStart(immediateGameplayState), + Action = () => scheduleStart(immediateGameplayState), Enabled = { Value = false } } } @@ -165,44 +161,27 @@ namespace osu.Game.Screens.Play automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } - protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) => Schedule(() => + protected override void OnUserStateChanged(int userId, SpectatorState spectatorState) { clearDisplay(); showBeatmapPanel(spectatorState); - }); + } - protected override void StartGameplay(int userId, GameplayState gameplayState) => Schedule(() => + protected override void StartGameplay(int userId, GameplayState gameplayState) { - pendingGameplayState = null; immediateGameplayState = gameplayState; - - if (this.IsCurrentScreen()) - attemptStart(gameplayState); - else - pendingGameplayState = gameplayState; - watchButton.Enabled.Value = true; - }); - protected override void EndGameplay(int userId) => Schedule(() => + scheduleStart(gameplayState); + } + + protected override void EndGameplay(int userId) { - pendingGameplayState = null; + scheduledStart?.Cancel(); immediateGameplayState = null; - - Schedule(clearDisplay); - watchButton.Enabled.Value = false; - }); - public override void OnResuming(IScreen last) - { - base.OnResuming(last); - - if (pendingGameplayState != null) - { - attemptStart(pendingGameplayState); - pendingGameplayState = null; - } + clearDisplay(); } private void clearDisplay() @@ -213,15 +192,27 @@ namespace osu.Game.Screens.Play previewTrackManager.StopAnyPlaying(this); } - private void attemptStart(GameplayState gameplayState) + private ScheduledDelegate scheduledStart; + + private void scheduleStart(GameplayState gameplayState) { - if (gameplayState == null) - return; + // This function may be called multiple times in quick succession once the screen becomes current again. + scheduledStart?.Cancel(); + scheduledStart = Schedule(() => + { + if (this.IsCurrentScreen()) + start(); + else + scheduleStart(gameplayState); + }); - Beatmap.Value = gameplayState.Beatmap; - Ruleset.Value = gameplayState.Ruleset.RulesetInfo; + void start() + { + Beatmap.Value = gameplayState.Beatmap; + Ruleset.Value = gameplayState.Ruleset.RulesetInfo; - this.Push(new SpectatorPlayerLoader(gameplayState.Score)); + this.Push(new SpectatorPlayerLoader(gameplayState.Score)); + } } private void showBeatmapPanel(SpectatorState state) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 4d285d519e..6dd3144fc8 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Spectate return; spectatorStates[userId] = state; - OnUserStateChanged(userId, state); + Schedule(() => OnUserStateChanged(userId, state)); updateGameplayState(userId); } @@ -148,7 +148,7 @@ namespace osu.Game.Screens.Spectate var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; - StartGameplay(userId, gameplayState); + Schedule(() => StartGameplay(userId, gameplayState)); } } @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Spectate gameplayState.Score.Replay.HasReceivedAllFrames = true; gameplayStates.Remove(userId); - EndGameplay(userId); + Schedule(() => EndGameplay(userId)); } } From 438f3e63499528226d13e21dac77880dc118a67a Mon Sep 17 00:00:00 2001 From: hbnrmx Date: Fri, 2 Apr 2021 17:57:21 +0200 Subject: [PATCH 42/46] move fallback text to PlaceholderText --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 1b841775e2..e00eaf9aa2 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -73,7 +73,8 @@ namespace osu.Game.Screens.Edit.Setup audioTrackTextBox = new FileChooserLabelledTextBox { Label = "Audio Track", - Current = { Value = working.Value.Metadata.AudioFile ?? "Click to select a track" }, + PlaceholderText = "Click to select a track", + Current = { Value = working.Value.Metadata.AudioFile }, Target = audioTrackFileChooserContainer, TabbableContentContainer = this }, From 824fb9f3987817585365e45ed01642b21573f387 Mon Sep 17 00:00:00 2001 From: hbnrmx Date: Fri, 2 Apr 2021 18:01:26 +0200 Subject: [PATCH 43/46] reopen FileSelector in the directory of the previous selection --- osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index 6e2737256a..70876bf26c 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit.Setup { FileSelector fileSelector; - Target.Child = fileSelector = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) + Target.Child = fileSelector = new FileSelector(currentFile.Value?.DirectoryName, ResourcesSection.AudioExtensions) { RelativeSizeAxes = Axes.X, Height = 400, From 0dce4b8894a55d9d6295673476557fd8041b6b6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 3 Apr 2021 13:01:08 +0900 Subject: [PATCH 44/46] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 5b65670869..73ee1d9d10 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 6a7f7e7026..931b55222a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 4aa3ad1c61..64e9a01a92 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From bd7da9eb39703e033c0ffa5b30563659f505efc2 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 3 Apr 2021 12:43:17 +0800 Subject: [PATCH 45/46] Make beatmap title use unicode --- osu.Game/Beatmaps/BeatmapMetadata.cs | 2 ++ osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 367f612dc8..eea9bdd976 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -19,8 +19,10 @@ namespace osu.Game.Beatmaps public int ID { get; set; } public string Title { get; set; } + [JsonProperty("title_unicode")] public string TitleUnicode { get; set; } public string Artist { get; set; } + [JsonProperty("artist_unicode")] public string ArtistUnicode { get; set; } [JsonIgnore] diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 153aa41582..a61640a02e 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -228,8 +229,8 @@ namespace osu.Game.Overlays.BeatmapSet loading.Hide(); - title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; - artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; + title.Text = new RomanisableString(setInfo.NewValue.Metadata.TitleUnicode, setInfo.NewValue.Metadata.Title); + artist.Text = new RomanisableString(setInfo.NewValue.Metadata.ArtistUnicode, setInfo.NewValue.Metadata.Artist); explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; From dde255980b9aa24bfe95764e20f811fca6b757f7 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 3 Apr 2021 12:44:51 +0800 Subject: [PATCH 46/46] Fix formatting --- osu.Game/Beatmaps/BeatmapMetadata.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index eea9bdd976..858da8e602 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -19,9 +19,12 @@ namespace osu.Game.Beatmaps public int ID { get; set; } public string Title { get; set; } + [JsonProperty("title_unicode")] public string TitleUnicode { get; set; } + public string Artist { get; set; } + [JsonProperty("artist_unicode")] public string ArtistUnicode { get; set; }